<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Saif Jlassi</title>
    <description>The latest articles on DEV Community by Saif Jlassi (@laakri).</description>
    <link>https://dev.to/laakri</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2553292%2Fa9613969-0e31-4cc3-8a4c-9c35d8f15e6e.png</url>
      <title>DEV Community: Saif Jlassi</title>
      <link>https://dev.to/laakri</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/laakri"/>
    <language>en</language>
    <item>
      <title>How We Made PNG Export More Responsive in Strokeline</title>
      <dc:creator>Saif Jlassi</dc:creator>
      <pubDate>Fri, 09 Oct 2026 11:57:51 +0000</pubDate>
      <link>https://dev.to/laakri/how-we-made-png-export-more-responsive-in-strokeline-4ldc</link>
      <guid>https://dev.to/laakri/how-we-made-png-export-more-responsive-in-strokeline-4ldc</guid>
      <description>&lt;p&gt;Exporting a scene as a PNG sounds simple: draw it to a canvas and download the&lt;br&gt;
result. But a large scene can turn that single click into a long stretch of&lt;br&gt;
main-thread work—and make the rest of the page feel frozen.&lt;/p&gt;

&lt;p&gt;We recently made Strokeline's PNG snapshot export more responsive. Here's what&lt;br&gt;
the export actually does, why we changed it, and what the change does (and&lt;br&gt;
doesn't) guarantee.&lt;/p&gt;
&lt;h2&gt;
  
  
  First, what does a PNG snapshot contain?
&lt;/h2&gt;

&lt;p&gt;Strokeline's PNG export captures &lt;strong&gt;one scene at the current playhead time&lt;/strong&gt;. It&lt;br&gt;
is not a video render: it resolves the timeline at that instant, draws the&lt;br&gt;
resulting scene to a canvas, then downloads a PNG.&lt;/p&gt;



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

&lt;p&gt;The lifecycle is roughly:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Compile and validate the current script.&lt;/li&gt;
&lt;li&gt;Select the active scene and clamp the playhead time to that scene's duration.&lt;/li&gt;
&lt;li&gt;Preload images and the handwritten font.&lt;/li&gt;
&lt;li&gt;Resolve the timeline into the state for that time.&lt;/li&gt;
&lt;li&gt;Draw the scene to a canvas.&lt;/li&gt;
&lt;li&gt;Encode the canvas as a PNG &lt;code&gt;Blob&lt;/code&gt; and download it.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  The problem: one uninterrupted render
&lt;/h2&gt;

&lt;p&gt;Previously, the snapshot path drew all visible scene nodes in one synchronous&lt;br&gt;
pass. While that pass was running, the browser had fewer opportunities to&lt;br&gt;
respond to input or paint UI updates. Dense scenes could therefore make the&lt;br&gt;
export feel like it had stalled the page.&lt;/p&gt;

&lt;p&gt;The first improvement is to render in &lt;strong&gt;batches of four nodes&lt;/strong&gt;. Between batches&lt;br&gt;
the renderer yields using &lt;code&gt;requestAnimationFrame&lt;/code&gt; (or a timer fallback), so the&lt;br&gt;
browser gets a chance to update the page. It also reports progress and checks&lt;br&gt;
whether the user cancelled before continuing.&lt;/p&gt;



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

&lt;p&gt;In simplified form, the asynchronous drawing loop is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;batchSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;batchSize&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;12&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;totalNodes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&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;steps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;drawSceneSteps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;batchSize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;while &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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;aborted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DOMException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;The export was cancelled.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;AbortError&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&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;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;

  &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onProgress&lt;/span&gt;&lt;span class="p"&gt;?.(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.98&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;totalNodes&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;new&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="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;requestAnimationFrame&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;function&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;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;resolve&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="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&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="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The PNG exporter sets the batch size to four. This is &lt;strong&gt;cooperative scheduling,&lt;br&gt;
not parallel rendering&lt;/strong&gt;: the work still happens on the browser's main thread.&lt;br&gt;
Yielding between batches gives the browser opportunities to respond, but one&lt;br&gt;
especially expensive node can still take noticeable time to draw.&lt;/p&gt;
&lt;h2&gt;
  
  
  Put a limit on output pixels
&lt;/h2&gt;

&lt;p&gt;Canvas memory use grows with the number of output pixels. Strokeline now exports&lt;br&gt;
at 1× scale by default, caps the PNG at &lt;strong&gt;4 million pixels&lt;/strong&gt;, and also limits&lt;br&gt;
either output dimension to &lt;strong&gt;8192 pixels&lt;/strong&gt;. It preserves the canvas aspect ratio&lt;br&gt;
when it needs to scale down a large output.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Canvas size&lt;/th&gt;
&lt;th&gt;Pixel count&lt;/th&gt;
&lt;th&gt;PNG output&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1920 × 1080&lt;/td&gt;
&lt;td&gt;2.07 MP&lt;/td&gt;
&lt;td&gt;1920 × 1080&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8000 × 5000&lt;/td&gt;
&lt;td&gt;40 MP&lt;/td&gt;
&lt;td&gt;2529 × 1581 (about 4 MP)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



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

&lt;p&gt;The cap is a practical limit, not a universal browser guarantee. Devices and&lt;br&gt;
browsers have different memory and canvas constraints, and a scene with costly&lt;br&gt;
shapes can still take time to draw.&lt;/p&gt;
&lt;h2&gt;
  
  
  Download a Blob instead of a base64 data URL
&lt;/h2&gt;

&lt;p&gt;After drawing, the exporter uses &lt;code&gt;canvas.toBlob()&lt;/code&gt; and downloads the resulting&lt;br&gt;
PNG &lt;code&gt;Blob&lt;/code&gt;. This avoids creating a large base64-encoded string as an intermediate&lt;br&gt;
representation. The final image still has to be encoded and stored, but it&lt;br&gt;
avoids that extra string allocation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blob&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Blob&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;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&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;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBlob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image/png&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;blob&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="s2"&gt;The browser could not encode the PNG snapshot.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;download&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`strokeline-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;.png`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Progress and cancellation
&lt;/h2&gt;

&lt;p&gt;The export menu reflects the drawing progress. Its cancellation signal is&lt;br&gt;
checked between batches, so a cancel request takes effect at a batch boundary.&lt;br&gt;
It can't interrupt an individual shape while that shape's renderer is running.&lt;/p&gt;

&lt;p&gt;That boundary is intentional: it keeps the drawing loop straightforward while&lt;br&gt;
giving long exports a way to stop without waiting for every remaining node.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;PNG snapshots render four nodes per batch and yield between batches.&lt;/li&gt;
&lt;li&gt;Progress is updated during rendering, and cancellation is checked at batch
boundaries.&lt;/li&gt;
&lt;li&gt;Output is 1×, with a 4 MP pixel cap and an 8192 px maximum dimension.&lt;/li&gt;
&lt;li&gt;PNG data is encoded with &lt;code&gt;toBlob()&lt;/code&gt; rather than an intermediate base64 URL.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The implementation is in the&lt;br&gt;
&lt;a href="https://github.com/laakri/strokeline" rel="noopener noreferrer"&gt;Strokeline repository&lt;/a&gt;, primarily in&lt;br&gt;
&lt;code&gt;web/src/export/exporters.ts&lt;/code&gt;, &lt;code&gt;web/src/export/pngSizing.ts&lt;/code&gt;, and&lt;br&gt;
&lt;code&gt;web/src/renderer/draw.ts&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If a large export still makes your browser unresponsive, the scene, device, and&lt;br&gt;
browser all matter. The batching and pixel cap reduce common sources of load;&lt;br&gt;
they don't make canvas rendering free.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>performance</category>
      <category>opensource</category>
    </item>
    <item>
      <title>From Correct to Polished: What Changed in Strokeline</title>
      <dc:creator>Saif Jlassi</dc:creator>
      <pubDate>Wed, 30 Sep 2026 13:34:08 +0000</pubDate>
      <link>https://dev.to/laakri/working-but-not-pro-yet-what-im-fixing-next-in-strokeline-53o8</link>
      <guid>https://dev.to/laakri/working-but-not-pro-yet-what-im-fixing-next-in-strokeline-53o8</guid>
      <description>&lt;p&gt;In my last post, I described Strokeline as &lt;strong&gt;working, but not pro yet&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;At that point, I had a clear list of things I wanted to improve: better motion, better rendering, better text placement, more reliable export, and eventually a better authoring experience.&lt;/p&gt;

&lt;p&gt;That post was mostly a plan.&lt;/p&gt;

&lt;p&gt;This one is about what actually happened.&lt;/p&gt;

&lt;p&gt;A lot of the work ended up being less about adding visual effects and more about fixing the fundamentals underneath them: geometry, timing, rendering consistency, and giving both humans and AI a reliable way to write valid Strokeline scripts.&lt;/p&gt;

&lt;p&gt;The biggest lesson so far is pretty simple:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Polish is mostly engineering.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The renderer became diagram-aware
&lt;/h2&gt;

&lt;p&gt;One of the biggest changes was making the renderer understand more about what it is actually drawing.&lt;/p&gt;

&lt;p&gt;Tables are a good example.&lt;/p&gt;

&lt;p&gt;Previously, getting a table on screen was not enough. The text needed to sit comfortably inside the cells, columns needed to give longer content room before wrapping, and the table needed to look like one coherent object.&lt;/p&gt;

&lt;p&gt;Tables now have better padding, automatic column widening before wrapping, rounded corners, header styling, soft shadows, and optional divider sections between attributes and operations.&lt;/p&gt;

&lt;p&gt;That same idea applies to shapes.&lt;/p&gt;

&lt;p&gt;Shapes now support rounded corners and better centered, auto-fitting labels. I also added a &lt;code&gt;DIAMOND&lt;/code&gt; shape for flowcharts.&lt;/p&gt;

&lt;p&gt;But arrows were probably the more interesting problem.&lt;/p&gt;

&lt;p&gt;An arrow shouldn't simply draw a line from coordinate A to coordinate B. It needs to understand the shapes it connects.&lt;/p&gt;

&lt;p&gt;The renderer now clips arrows to the actual shape boundary, spreads anchors when multiple arrows connect to the same side, and uses straighter lines with rounded elbows.&lt;/p&gt;

&lt;p&gt;That makes a surprisingly large difference.&lt;/p&gt;

&lt;p&gt;Then came UML-specific details: hollow triangles, hollow and filled diamonds, open arrowheads, multiplicities, and labels on both ends of relationships.&lt;/p&gt;

&lt;p&gt;For example, a relationship can now carry information like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SOURCELABEL "1"
TARGETLABEL "*"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those labels also get automatic background plates so they remain readable over the diagram.&lt;/p&gt;

&lt;p&gt;Self-message loops are supported too, which matters for sequence diagrams where an object can send a message back to itself.&lt;/p&gt;

&lt;p&gt;The important part is that these aren't isolated visual additions.&lt;/p&gt;

&lt;p&gt;Arrow geometry depends on knowing the shape boundary. Label placement depends on knowing the arrow geometry. Readability depends on knowing where the label lands. Multiple arrows depend on knowing which side of the shape they share.&lt;/p&gt;

&lt;p&gt;Fixing one piece exposed problems in the next.&lt;/p&gt;

&lt;p&gt;That's what I mean by the renderer becoming &lt;strong&gt;diagram-aware&lt;/strong&gt;. It isn't just drawing primitives anymore. More of the rendering logic understands the relationships between those primitives.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preview and export have to agree
&lt;/h2&gt;

&lt;p&gt;Another lesson was that a preview that looks good is not enough.&lt;/p&gt;

&lt;p&gt;If the preview and exported video use different rendering paths, eventually they disagree.&lt;/p&gt;

&lt;p&gt;Strokeline now renders with a crisp high-resolution 2x canvas, and exports use the same renderer and visual rules as the preview.&lt;/p&gt;

&lt;p&gt;Video export is also deterministic rather than depending only on real-time screen capture.&lt;/p&gt;

&lt;p&gt;That matters because an animation editor should not effectively say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"It looked right while you were watching it, hopefully the recording looks the same."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;GIF export gave me a particularly good example of this.&lt;/p&gt;

&lt;p&gt;I had a bug where exported GIF frames could come out black or empty. The underlying problem wasn't the animation itself. The frames were being handled at the wrong stage of the rendering/downsampling process.&lt;/p&gt;

&lt;p&gt;The fix was to render each frame at the logical canvas size first and then downsample it.&lt;/p&gt;

&lt;p&gt;That sounds like a small implementation detail.&lt;/p&gt;

&lt;p&gt;It wasn't small from the user's perspective.&lt;/p&gt;

&lt;p&gt;A perfect-looking preview followed by an empty GIF is simply a broken export.&lt;/p&gt;

&lt;p&gt;Sequence-diagram frames and use-case boundaries also received cleanup, while camera motion and easing became smoother and more stable. Tables and arrows now have smoother reveal animations as well.&lt;/p&gt;

&lt;p&gt;I'm still treating preview/export consistency as an area that needs testing. In particular, I haven't verified the stronger claim from the previous post that old scripts remain pixel-identical, so that's still an open item rather than something I'm going to pretend is solved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Narration changed the architecture of timing
&lt;/h2&gt;

&lt;p&gt;In the previous post, I deliberately said I was skipping voice-over and subtitles for the time being.&lt;/p&gt;

&lt;p&gt;That didn't last.&lt;/p&gt;

&lt;p&gt;Once narration became part of the product, it became obvious that captions, speech, and animation timing can't really be treated as separate systems.&lt;/p&gt;

&lt;p&gt;Consider a &lt;code&gt;SAY&lt;/code&gt; line.&lt;/p&gt;

&lt;p&gt;It has a duration declared by the script, but the text itself also has a natural reading time. If I only use the declared duration, a long sentence can finish visually before there is enough time to read or hear it.&lt;/p&gt;

&lt;p&gt;Strokeline now schedules a &lt;code&gt;SAY&lt;/code&gt; using whichever is longer: its declared duration or a text-based estimate.&lt;/p&gt;

&lt;p&gt;That timing then needs to drive the caption.&lt;/p&gt;

&lt;p&gt;And the caption needs to represent the entire spoken text. Long captions now advance through the full text instead of getting stuck showing only the first couple of wrapped lines.&lt;/p&gt;

&lt;p&gt;Then there is generated audio.&lt;/p&gt;

&lt;p&gt;The audio needs to line up with that same timeline. If the caption says one thing while the generated narration is at another point, the system feels broken even if both pieces work individually.&lt;/p&gt;

&lt;p&gt;So generated audio is now synchronized with the actual caption timeline.&lt;/p&gt;

&lt;p&gt;Seeking and playback timing were also improved, making audio less likely to skip or end abruptly. Pausing the preview now pauses the narration state as well.&lt;/p&gt;

&lt;p&gt;This is still ongoing work. I'm not claiming that every browser audio edge case is solved.&lt;/p&gt;

&lt;p&gt;But the architectural lesson is clear:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If two things describe the same moment in the animation, they need to share the same timing model.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  AI needs a source of truth
&lt;/h2&gt;

&lt;p&gt;Another part of Strokeline that changed wasn't the renderer at all.&lt;/p&gt;

&lt;p&gt;It was how scripts get written.&lt;/p&gt;

&lt;p&gt;Because Strokeline is text-based, it's natural to ask an AI to generate a &lt;code&gt;.wbs&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;That also creates a very predictable problem: an AI can confidently invent syntax.&lt;/p&gt;

&lt;p&gt;It might produce YAML. Or JSON. Or Mermaid. Or PlantUML. Or some imaginary hybrid that looks reasonable but has nothing to do with the actual Strokeline parser.&lt;/p&gt;

&lt;p&gt;That's not useful.&lt;/p&gt;

&lt;p&gt;So I built a public Strokeline skill repository:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/laakri/Strokeline-Skills" rel="noopener noreferrer"&gt;Strokeline-Skills&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The skill contains the actual core instructions, syntax references, diagnostics, layout guidance, quality checks, bad/good examples, patterns, and complete example scripts.&lt;/p&gt;

&lt;p&gt;The important part is that the examples are validated against the real parser/compiler/validator.&lt;/p&gt;

&lt;p&gt;The workspace's &lt;strong&gt;Copy AI Prompt&lt;/strong&gt; now tells assistants to use this skill before generating or editing Strokeline scripts.&lt;/p&gt;

&lt;p&gt;That gives an AI something much better than a vague description of the product: an actual source of truth.&lt;/p&gt;

&lt;p&gt;The skill also documents details that are easy for an AI to get subtly wrong.&lt;/p&gt;

&lt;p&gt;For example, &lt;code&gt;CANVAS&lt;/code&gt; takes width and then height. &lt;code&gt;POSITION&lt;/code&gt; represents the center point of an object. Vertical layouts need to respect safe areas.&lt;/p&gt;

&lt;p&gt;These details sound obvious once you know them.&lt;/p&gt;

&lt;p&gt;They're exactly the kind of details an AI will happily get wrong if nobody gives it authoritative instructions.&lt;/p&gt;

&lt;p&gt;The goal isn't to make AI magically understand Strokeline.&lt;/p&gt;

&lt;p&gt;The goal is to give it enough real information that the output has a chance of being executable on the first pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small UI cleanup
&lt;/h2&gt;

&lt;p&gt;I also cleaned up the surrounding workspace.&lt;/p&gt;

&lt;p&gt;The landing page now has a darker shadcn-style design with stronger hierarchy, refined cards, spacing, and motion.&lt;/p&gt;

&lt;p&gt;Inside the workspace, menus such as Scenes now appear above the preview instead of getting clipped behind it.&lt;/p&gt;

&lt;p&gt;It's not the main story, but the editor shouldn't get in the way of the thing you're actually building.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's still unfinished
&lt;/h2&gt;

&lt;p&gt;Strokeline is in a much better place than when I wrote the previous post, but there are still plenty of open problems.&lt;/p&gt;

&lt;p&gt;I haven't finished proving compatibility with existing scripts at the pixel level. Audio behavior still needs continued work across different browser conditions. And the renderer still has plenty of room for refinement as more complicated diagrams expose new geometry and timing cases.&lt;/p&gt;

&lt;p&gt;I'm also deliberately not calling every item from the original roadmap finished just because related work happened. Some of those ideas still need proper implementation and validation.&lt;/p&gt;

&lt;p&gt;Right now, my focus is on making the existing pipeline more reliable and predictable rather than piling on effects.&lt;/p&gt;

&lt;p&gt;The pattern I've started to see is that every layer affects the next one.&lt;/p&gt;

&lt;p&gt;Better geometry improves animation.&lt;/p&gt;

&lt;p&gt;Better timing improves narration.&lt;/p&gt;

&lt;p&gt;A shared renderer improves export.&lt;/p&gt;

&lt;p&gt;A real syntax reference improves AI-generated scripts.&lt;/p&gt;

&lt;p&gt;And when those foundations are right, visual polish becomes much easier to build on top of them.&lt;/p&gt;

&lt;p&gt;That's a much more useful definition of "polished" than adding more effects.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What would you want a scriptable diagram animation tool to get right before anything else: rendering, animation, narration, export, or AI authoring?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Strokeline studio: &lt;a href="https://strokeline.glassisaif.workers.dev/" rel="noopener noreferrer"&gt;https://strokeline.glassisaif.workers.dev/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Strokeline AI skill repo: &lt;a href="https://github.com/laakri/Strokeline-Skills" rel="noopener noreferrer"&gt;https://github.com/laakri/Strokeline-Skills&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>typescript</category>
      <category>opensource</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I'm building a scripting language for whiteboard animations</title>
      <dc:creator>Saif Jlassi</dc:creator>
      <pubDate>Sat, 19 Sep 2026 10:24:50 +0000</pubDate>
      <link>https://dev.to/laakri/im-building-a-scripting-language-for-whiteboard-animations-21gb</link>
      <guid>https://dev.to/laakri/im-building-a-scripting-language-for-whiteboard-animations-21gb</guid>
      <description>&lt;p&gt;I started &lt;strong&gt;Strokeline&lt;/strong&gt; with one dumb, simple idea:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What if you could describe an animation the same way you describe a webpage — with text — and something else just... drew it?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Not AI-generated video.&lt;/p&gt;

&lt;p&gt;Not a drag-and-drop editor where you fight with alignment guides for twenty minutes.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;script&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CIRCLE browser
RECTANGLE server
ARROW browser -&amp;gt; server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hit run, and a hand-drawn whiteboard animation plays.&lt;/p&gt;

&lt;p&gt;The twist — and really the whole point — is that the scripting language is simple enough that you can ask &lt;strong&gt;ChatGPT, Claude, or another AI&lt;/strong&gt; to write it for you.&lt;/p&gt;

&lt;p&gt;Something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Explain how DNS works in 45 seconds.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;goes in.&lt;/p&gt;

&lt;p&gt;A script comes out.&lt;/p&gt;

&lt;p&gt;You paste it in.&lt;/p&gt;

&lt;p&gt;You hit run.&lt;/p&gt;

&lt;p&gt;And hopefully...&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;an animation comes out.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Simple idea.&lt;/p&gt;

&lt;p&gt;Turns out &lt;strong&gt;"simple idea" and "simple to build" have almost nothing to do with each other.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The bug that ate 4GB of RAM
&lt;/h2&gt;

&lt;p&gt;Somewhere in week one, my test suite died.&lt;/p&gt;

&lt;p&gt;Not failed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Died.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Vitest ran out of memory and took the whole process down with it.&lt;/p&gt;

&lt;p&gt;It took a while to track down, but here's what was happening.&lt;/p&gt;

&lt;p&gt;My parser had a recovery mechanism.&lt;/p&gt;

&lt;p&gt;If it hit a broken line, it was supposed to skip ahead to the next recognizable keyword and keep going instead of crashing on the first typo.&lt;/p&gt;

&lt;p&gt;Good idea in theory.&lt;/p&gt;

&lt;p&gt;The bug was that one specific keyword — &lt;code&gt;TEXT&lt;/code&gt;, if you're curious — could be interpreted in two different ways.&lt;/p&gt;

&lt;p&gt;It could be a property:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CREATE server AS RECTANGLE
  TEXT "SERVER"
END
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But it was also being treated as a statement boundary that the error-recovery logic could search for.&lt;/p&gt;

&lt;p&gt;Under the wrong conditions, the parser would "recover" to a position it had never actually left.&lt;/p&gt;

&lt;p&gt;So it did this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;parse
  ↓
error
  ↓
recover
  ↓
same position
  ↓
error
  ↓
recover
  ↓
same position
  ↓
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Forever.&lt;/p&gt;

&lt;p&gt;Every iteration created another diagnostic object.&lt;/p&gt;

&lt;p&gt;Eventually:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4GB of RAM.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Then the OS gave up.&lt;/p&gt;

&lt;p&gt;The actual fix was three lines.&lt;/p&gt;

&lt;p&gt;Finding those three lines was not.&lt;/p&gt;




&lt;h2&gt;
  
  
  The screen that just went black
&lt;/h2&gt;

&lt;p&gt;Once the parser was solid, I started building the actual editor.&lt;/p&gt;

&lt;p&gt;The basic idea is pretty simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────┬──────────────────────────┐
│                     │                          │
│    SCRIPT EDITOR    │      ANIMATION           │
│                     │                          │
│  CREATE browser...  │       ✏️ ○ ───────→ □   │
│                     │                          │
│                     │                          │
└─────────────────────┴──────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Type on the left.&lt;/p&gt;

&lt;p&gt;Watch it animate on the right.&lt;/p&gt;

&lt;p&gt;I pasted in a two-scene test script to check everything worked end to end.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Black screen.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Console full of red text.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Maximum update depth exceeded.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Great.&lt;/p&gt;

&lt;p&gt;This one wasn't the parser at all.&lt;/p&gt;

&lt;p&gt;It was React and my state manager, Zustand, getting into a fight neither of them could win.&lt;/p&gt;

&lt;p&gt;Somewhere in the code that renders the scene tabs, a piece of derived data — basically:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Give me the list of scenes."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;— was being recalculated on every render.&lt;/p&gt;

&lt;p&gt;That meant it was a new array every time, even when nothing had actually changed.&lt;/p&gt;

&lt;p&gt;React compares objects and arrays by reference.&lt;/p&gt;

&lt;p&gt;So:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;new array
   ↓
"Something changed"
   ↓
re-render
   ↓
calculate scenes
   ↓
new array
   ↓
"Something changed"
   ↓
re-render
   ↓
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Forever.&lt;/p&gt;

&lt;p&gt;It only showed up with &lt;strong&gt;two or more scenes&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;One scene?&lt;/p&gt;

&lt;p&gt;Totally fine.&lt;/p&gt;

&lt;p&gt;Two scenes?&lt;/p&gt;

&lt;p&gt;Welcome to hell.&lt;/p&gt;

&lt;p&gt;That's the kind of bug that makes you paranoid about every other component in your codebase doing the exact same thing quietly, waiting for the right input.&lt;/p&gt;




&lt;h2&gt;
  
  
  Text that vanishes the moment it finishes appearing
&lt;/h2&gt;

&lt;p&gt;This one's my favorite because it's so specific that it's almost funny.&lt;/p&gt;

&lt;p&gt;I finally got the "hand-drawn" reveal working.&lt;/p&gt;

&lt;p&gt;Text types itself onto the canvas character by character.&lt;/p&gt;

&lt;p&gt;Shapes trace their own outlines like a pen is drawing them.&lt;/p&gt;

&lt;p&gt;It was genuinely satisfying to watch.&lt;/p&gt;

&lt;p&gt;Except...&lt;/p&gt;

&lt;p&gt;A few seconds after any text finished typing, it would just...&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;disappear.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not fade.&lt;/p&gt;

&lt;p&gt;Not glitch.&lt;/p&gt;

&lt;p&gt;Gone.&lt;/p&gt;

&lt;p&gt;Like it was never there.&lt;/p&gt;

&lt;p&gt;I had two separate rendering paths.&lt;/p&gt;

&lt;p&gt;One for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;still animating
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;where the text was drawn live every frame.&lt;/p&gt;

&lt;p&gt;And another for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;finished
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;where I could reuse a cached image instead of asking an expensive hand-drawn rendering library to redo the same work sixty times a second for something that wasn't moving.&lt;/p&gt;

&lt;p&gt;Smart idea.&lt;/p&gt;

&lt;p&gt;Better performance.&lt;/p&gt;

&lt;p&gt;Except the code calculating how large that cached image needed to be was using a rough guess:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;text.length * fontSize * 0.55
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I am not proud of that line.&lt;/p&gt;

&lt;p&gt;It also assumed the text started from a point instead of being centered on that point, which is how it actually gets drawn.&lt;/p&gt;

&lt;p&gt;So everything looked perfectly fine while the animation was running.&lt;/p&gt;

&lt;p&gt;Then the animation finished.&lt;/p&gt;

&lt;p&gt;The renderer switched to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"just show the cached version"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And suddenly the cached version had the wrong dimensions and the wrong position.&lt;/p&gt;

&lt;p&gt;Half the text disappeared silently.&lt;/p&gt;

&lt;p&gt;Every.&lt;/p&gt;

&lt;p&gt;Single.&lt;/p&gt;

&lt;p&gt;Time.&lt;/p&gt;

&lt;p&gt;The animation itself was working.&lt;/p&gt;

&lt;p&gt;The cache was working.&lt;/p&gt;

&lt;p&gt;The transition between the two was the problem.&lt;/p&gt;




&lt;h2&gt;
  
  
  None of these bugs were exotic
&lt;/h2&gt;

&lt;p&gt;That's probably the part I find most interesting.&lt;/p&gt;

&lt;p&gt;There was no mysterious AI behavior.&lt;/p&gt;

&lt;p&gt;No obscure browser API.&lt;/p&gt;

&lt;p&gt;No impossible-to-reproduce hardware issue.&lt;/p&gt;

&lt;p&gt;Just normal software bugs.&lt;/p&gt;

&lt;p&gt;Different bugs.&lt;/p&gt;

&lt;p&gt;Different systems.&lt;/p&gt;

&lt;p&gt;Different symptoms.&lt;/p&gt;

&lt;p&gt;But underneath, they all had something in common:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;the program believed something was true when it wasn't.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The parser thought it had recovered.&lt;/p&gt;

&lt;p&gt;It hadn't.&lt;/p&gt;

&lt;p&gt;React thought the data had changed.&lt;/p&gt;

&lt;p&gt;It hadn't.&lt;/p&gt;

&lt;p&gt;The renderer thought the cached text had the correct dimensions.&lt;/p&gt;

&lt;p&gt;It didn't.&lt;/p&gt;

&lt;p&gt;Three completely different corners of the codebase.&lt;/p&gt;

&lt;p&gt;Three completely different disguises.&lt;/p&gt;

&lt;p&gt;Same fundamental problem.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I'm telling you this instead of just shipping quietly
&lt;/h2&gt;

&lt;p&gt;Because I think there's a specific kind of satisfaction in watching something like this get built in public.&lt;/p&gt;

&lt;p&gt;Not just the polished announcement at the end.&lt;/p&gt;

&lt;p&gt;The bugs too.&lt;/p&gt;

&lt;p&gt;The broken builds.&lt;/p&gt;

&lt;p&gt;The weird rendering problems.&lt;/p&gt;

&lt;p&gt;The moments where you stare at a console error wondering how something this stupid could possibly be happening.&lt;/p&gt;

&lt;p&gt;The whole premise of Strokeline is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;text
  ↓
program
  ↓
animation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And building the thing that makes that possible has turned out to be a pretty good example of exactly that process.&lt;/p&gt;

&lt;p&gt;You write something you believe is correct.&lt;/p&gt;

&lt;p&gt;Then reality — the browser, the state manager, the parser, the rendering pipeline — tells you:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"No."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And you investigate why.&lt;/p&gt;




&lt;h2&gt;
  
  
  The part I'm most excited about
&lt;/h2&gt;

&lt;p&gt;The interesting thing about a scripting language for animations isn't really the shapes.&lt;/p&gt;

&lt;p&gt;Drawing a circle isn't difficult.&lt;/p&gt;

&lt;p&gt;Drawing an arrow isn't difficult.&lt;/p&gt;

&lt;p&gt;Even animating them isn't particularly difficult.&lt;/p&gt;

&lt;p&gt;The interesting part is creating a language that is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;simple enough for humans to understand&lt;/li&gt;
&lt;li&gt;structured enough for a renderer to execute&lt;/li&gt;
&lt;li&gt;predictable enough for an AI to generate&lt;/li&gt;
&lt;li&gt;expressive enough to create real explanations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, I want something like this to be enough to explain a basic HTTP request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SCENE 1 "HTTP Request"

  CREATE browser AS CIRCLE
    POSITION 400 500
    RADIUS 100
    LABEL "Browser"
    DRAW 1s
  END

  CREATE server AS RECTANGLE
    POSITION 1400 500
    WIDTH 300
    HEIGHT 220
    TEXT "SERVER"
    DRAW 1s
  END

  ARROW browser -&amp;gt; server
    LABEL "HTTP Request"
    DRAW 1s
  END

END SCENE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the direction I'm aiming for.&lt;/p&gt;

&lt;p&gt;Not a giant animation framework that requires a 400-page manual.&lt;/p&gt;

&lt;p&gt;Something closer to:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;HTML, but for explaining things visually.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  And yes, AI is a big part of this
&lt;/h2&gt;

&lt;p&gt;This is where Strokeline gets particularly interesting to me.&lt;/p&gt;

&lt;p&gt;You don't necessarily need to learn the scripting language.&lt;/p&gt;

&lt;p&gt;You could simply ask:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Explain how a database index works in 60 seconds.

Use three scenes.

Start with a slow query.

Then show the index.

Finish by comparing indexed and non-indexed lookups.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The AI generates the script.&lt;/p&gt;

&lt;p&gt;Strokeline validates it.&lt;/p&gt;

&lt;p&gt;The renderer executes it.&lt;/p&gt;

&lt;p&gt;And you get an animation.&lt;/p&gt;

&lt;p&gt;That means the language becomes a kind of &lt;strong&gt;intermediate representation for visual explanations&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The AI doesn't have to generate pixels.&lt;/p&gt;

&lt;p&gt;It generates instructions.&lt;/p&gt;

&lt;p&gt;The renderer handles the pixels.&lt;/p&gt;

&lt;p&gt;That's a much more interesting problem to me.&lt;/p&gt;




&lt;h2&gt;
  
  
  The project is still early
&lt;/h2&gt;

&lt;p&gt;The core loop works now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Write script
    ↓
Parse script
    ↓
Build scene
    ↓
Render animation
    ↓
Play / scrub / inspect
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I've also been working on things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;camera movement&lt;/li&gt;
&lt;li&gt;scene management&lt;/li&gt;
&lt;li&gt;animation timing&lt;/li&gt;
&lt;li&gt;hand-drawn rendering&lt;/li&gt;
&lt;li&gt;text measurement&lt;/li&gt;
&lt;li&gt;caching&lt;/li&gt;
&lt;li&gt;error recovery&lt;/li&gt;
&lt;li&gt;script validation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And yes...&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;bugs.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A lot of bugs.&lt;/p&gt;

&lt;p&gt;But that's part of building it.&lt;/p&gt;

&lt;p&gt;If you're the kind of person who reads:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"AI generates a whiteboard animation script and my platform just... plays it."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;and immediately thinks:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Wait, how does the timeline handle scrubbing backward in the middle of an animation without breaking everything?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You're exactly who I'm building this for.&lt;/p&gt;

&lt;p&gt;And I'd genuinely love to hear what you'd worry about first.&lt;/p&gt;




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

&lt;h2&gt;
  
  
  What's next?
&lt;/h2&gt;

&lt;p&gt;I'm still figuring out what Strokeline ultimately becomes.&lt;/p&gt;

&lt;p&gt;Right now, I'm focused on getting the fundamentals right:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;language → parser → timeline → renderer → animation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once that foundation is solid, there are a lot of directions this could go.&lt;/p&gt;

&lt;p&gt;More animation primitives.&lt;/p&gt;

&lt;p&gt;Better camera control.&lt;/p&gt;

&lt;p&gt;Reusable components.&lt;/p&gt;

&lt;p&gt;Timeline editing.&lt;/p&gt;

&lt;p&gt;AI-assisted script generation.&lt;/p&gt;

&lt;p&gt;Maybe eventually collaborative editing.&lt;/p&gt;

&lt;p&gt;But for now?&lt;/p&gt;

&lt;p&gt;I'm happy that I can type a few lines of text and watch the browser turn them into something that actually moves.&lt;/p&gt;

&lt;p&gt;Especially after spending several hours debugging why the text was disappearing.&lt;/p&gt;

&lt;p&gt;More soon.&lt;/p&gt;

&lt;p&gt;Probably with more bugs to tell you about.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Lakron : A Minimal, Encrypted, Realtime Life Scheduler with Supabase</title>
      <dc:creator>Saif Jlassi</dc:creator>
      <pubDate>Wed, 25 Jun 2025 12:59:16 +0000</pubDate>
      <link>https://dev.to/laakri/lakron-a-minimal-encrypted-realtime-life-scheduler-with-supabase-17bi</link>
      <guid>https://dev.to/laakri/lakron-a-minimal-encrypted-realtime-life-scheduler-with-supabase-17bi</guid>
      <description>&lt;p&gt;Lakron: A Minimal, Encrypted, Realtime Life Scheduler with Supabase&lt;/p&gt;

&lt;p&gt;Lakron is a minimal, smart life scheduler that keeps your data private and in sync. Every task is encrypted before it leaves your browser, and realtime sync means your schedule is always up to date everywhere.&lt;/p&gt;

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

&lt;p&gt;Every task you create is encrypted in the browser using a key derived from your password and a salt. Only you can decrypt your data—nobody else, not even the database admin.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// TaskProvider.tsx&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;generateKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useMemo&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;currentProfile&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="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;CryptoJS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PBKDF2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentProfile&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="nx"&gt;ENCRYPTION_SALT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;keySize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;toString&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;currentProfile&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When saving or loading a task, the title and description are encrypted/decrypted with this key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// supabase.ts&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;encrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;CryptoJS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;decrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cipher&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="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;CryptoJS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cipher&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="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CryptoJS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;enc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Utf8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How to Clone and Run Lakron
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Clone the repo:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   git clone https://github.com/laakri/lakron.git
   cd lakron
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Install dependencies:
&lt;/li&gt;
&lt;/ol&gt;

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

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Create a &lt;code&gt;.env&lt;/code&gt; file in the root with your Supabase project keys.
&lt;strong&gt;Use your own values, never share your real keys publicly!&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   VITE_ENCRYPTION_KEY=your-random-salt
   VITE_SUPABASE_URL=https://your-project.supabase.co
   VITE_SUPABASE_KEY=your-service-role-key
   VITE_SUPABASE_ANON_KEY=your-anon-key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Start the dev server:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Enable Realtime on the Tasks Table
&lt;/h2&gt;

&lt;p&gt;Lakron uses Supabase Realtime to sync tasks instantly across devices.&lt;br&gt;&lt;br&gt;
To enable this, turn on Realtime for the &lt;code&gt;tasks&lt;/code&gt; table in your Supabase dashboard:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Go to your Supabase project&lt;/li&gt;
&lt;li&gt;Click on "Database" → "Replication"&lt;/li&gt;
&lt;li&gt;Find the &lt;code&gt;tasks&lt;/code&gt; table and enable Realtime&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  SQL to Create the Tables (All in One Block)
&lt;/h2&gt;

&lt;p&gt;If you need to create the &lt;code&gt;profiles&lt;/code&gt; and &lt;code&gt;tasks&lt;/code&gt; tables from scratch, here’s the full SQL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Create profiles table&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;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;profiles&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;SERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&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&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="nb"&gt;TIME&lt;/span&gt; &lt;span class="k"&gt;ZONE&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Create tasks table&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;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;tasks&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nb"&gt;time&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&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="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="nb"&gt;date&lt;/span&gt; &lt;span class="nb"&gt;DATE&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;completed&lt;/span&gt; &lt;span class="nb"&gt;BOOLEAN&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;FALSE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&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="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'task'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'event'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'task'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;profile_id&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;profiles&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="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="nb"&gt;TIME&lt;/span&gt; &lt;span class="k"&gt;ZONE&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;priority&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;recurring&lt;/span&gt; &lt;span class="nb"&gt;BOOLEAN&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;recurrence_rule&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;"completedDates"&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Add constraint to ensure recurrence_rule values are valid&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;tasks&lt;/span&gt;
  &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;check_recurrence_rule&lt;/span&gt;
  &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;recurrence_rule&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'daily'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'weekly'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'monthly'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'yearly'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'custom'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;recurrence_rule&lt;/span&gt; &lt;span class="k"&gt;IS&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;-- Optional: Standardize existing recurrence_rule values (safe to run even if table is empty)&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;tasks&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;recurrence_rule&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'daily'&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;recurrence_rule&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'daily'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;tasks&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;recurrence_rule&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'weekly'&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;recurrence_rule&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'weekly'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;tasks&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;recurrence_rule&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'monthly'&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;recurrence_rule&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'monthly'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;tasks&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;recurrence_rule&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'yearly'&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;recurrence_rule&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'yearly'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;tasks&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;recurrence_rule&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'custom'&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;recurrence_rule&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;recurrence_rule&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'daily'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'weekly'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'monthly'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'yearly'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Realtime Sync
&lt;/h2&gt;

&lt;p&gt;Supabase’s realtime channels keep tasks in sync across tabs and devices. The provider subscribes to changes for the current profile, so if you add or complete a task on your phone, it updates instantly on your laptop.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// TaskProvider.tsx&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;unsubscribe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dbOperations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribeToTasks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentProfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&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;// handle INSERT, UPDATE, DELETE&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Deployment
&lt;/h2&gt;

&lt;p&gt;Deploying Lakron is as smooth as using it. The whole process is automated with GitHub Actions, so every push to the main branch triggers a build and deploy.&lt;/p&gt;

&lt;p&gt;The workflow does three things:  &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Installs dependencies and builds the frontend
&lt;/li&gt;
&lt;li&gt;Copies the built static files to the server using SCP over SSH
&lt;/li&gt;
&lt;li&gt;Reloads the Caddy server so your changes go live instantly&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here’s the gist of the deployment workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/deploy.yml&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install dependencies and build&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;npm ci&lt;/span&gt;
    &lt;span class="s"&gt;npm run build&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy dist to server&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;appleboy/scp-action@v0.1.7&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SERVER_HOST }}&lt;/span&gt;
    &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SERVER_USER }}&lt;/span&gt;
    &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SERVER_SSH_KEY }}&lt;/span&gt;
    &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dist/*"&lt;/span&gt;
    &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/home/youruser/lakron/dist"&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Reload Caddy&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;appleboy/ssh-action@v1.0.0&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SERVER_HOST }}&lt;/span&gt;
    &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SERVER_USER }}&lt;/span&gt;
    &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SERVER_SSH_KEY }}&lt;/span&gt;
    &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;sudo systemctl reload caddy&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You just need to add your server’s SSH key and connection info as GitHub secrets.&lt;br&gt;&lt;br&gt;
Caddy serves the static files from the &lt;code&gt;dist&lt;/code&gt; directory, so as soon as the workflow finishes, your latest version is live.&lt;/p&gt;

&lt;p&gt;No manual uploads, no downtime, just push and your encrypted, realtime scheduler is up to date for everyone.&lt;/p&gt;




&lt;p&gt;Clone, set your env, run the SQL, enable realtime, and you’ve got a fully encrypted, realtime, minimal life scheduler.&lt;/p&gt;

</description>
      <category>react</category>
      <category>supabase</category>
      <category>devops</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Building a Secure Multi-Device Authentication System: The SyncBridge Story #1🔐</title>
      <dc:creator>Saif Jlassi</dc:creator>
      <pubDate>Mon, 23 Dec 2024 18:04:21 +0000</pubDate>
      <link>https://dev.to/laakri/building-a-secure-multi-device-authentication-system-the-syncbridge-story-1-1pgp</link>
      <guid>https://dev.to/laakri/building-a-secure-multi-device-authentication-system-the-syncbridge-story-1-1pgp</guid>
      <description>&lt;p&gt;Ever wondered how apps like WhatsApp Web handle secure device pairing? Or how Netflix manages to keep track of all your devices? Today, I'm diving deep into how I built SyncBridge's authentication system, handling everything from QR code login to token refresh mechanisms. Buckle up - it's gonna be a fun ride! 🚀&lt;/p&gt;

&lt;h2&gt;
  
  
  The Challenge 🎯
&lt;/h2&gt;

&lt;p&gt;Building SyncBridge's auth system wasn't just about letting users log in. I needed to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Track multiple devices per user&lt;/li&gt;
&lt;li&gt;Handle secure device pairing&lt;/li&gt;
&lt;li&gt;Manage token refresh without compromising security&lt;/li&gt;
&lt;li&gt;Implement email verification&lt;/li&gt;
&lt;li&gt;Monitor suspicious activities&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Architecture 🏗️
&lt;/h2&gt;

&lt;p&gt;Here's what I came up with:&lt;/p&gt;

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

&lt;h3&gt;
  
  
  The Secret Sauce 🌟
&lt;/h3&gt;

&lt;p&gt;What makes this system special? Three key ingredients:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Device-Specific Tokens&lt;/strong&gt;: Each device gets its own unique identifier and refresh token. If one device is compromised, others stay safe.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Rotating Refresh Tokens&lt;/strong&gt;: Every time a refresh token is used, it's invalidated and a new one is issued. This means if someone steals your refresh token, they have exactly one shot at using it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Security Event Tracking&lt;/strong&gt;: Every login, token refresh, and device pairing is monitored. Suspicious patterns? We catch them before they become problems.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Let's Get Technical 🛠️
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Token Management: The Dance of Access and Refresh 💃
&lt;/h2&gt;

&lt;p&gt;Remember the last time you stayed logged into an app for weeks without entering your password? That's the magic of refresh tokens. But here's how we made it more secure in SyncBridge:&lt;/p&gt;

&lt;h3&gt;
  
  
  The Two-Token Tango 🎭
&lt;/h3&gt;

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

&lt;h3&gt;
  
  
  Why This Matters 🤔
&lt;/h3&gt;

&lt;p&gt;Here's what makes our token system special:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Single-Use Refresh Tokens&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// When refreshing tokens
const deviceAuth = this.deviceAuthRepository.create({
device_id: device.device_id,
refresh_token: refreshToken,
expires_at: new Date(Date.now() + 7 24 60 60 1000), // 7 days
is_valid: true,
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Device-Bound Tokens&lt;/strong&gt;&lt;br&gt;
Each refresh token is tied to a specific device. Try using it from another device? Not happening! 🚫&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Concurrent Request Handling&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if (isRefreshing) {
try {
const token = await new Promise((resolve, reject) =&amp;gt; {
failedQueue.push({ resolve, reject });
});
// Handle concurrent requests during refresh
}
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Security Checklist ✅
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;✅ Access tokens expire in 15 minutes&lt;/li&gt;
&lt;li&gt;✅ Refresh tokens are single-use&lt;/li&gt;
&lt;li&gt;✅ Device fingerprinting for extra security&lt;/li&gt;
&lt;li&gt;✅ Token rotation on every refresh&lt;/li&gt;
&lt;li&gt;✅ Concurrent request queue management&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  QR Code Magic: Secure Device Pairing 📱
&lt;/h2&gt;

&lt;p&gt;One of the coolest features we built is the QR code login system. You know when you open WhatsApp Web and scan that QR code? We built something similar, but with our own security twist!&lt;/p&gt;

&lt;h3&gt;
  
  
  How It Works 🔍
&lt;/h3&gt;

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

&lt;p&gt;When you click "Add Device", here's what happens behind the scenes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;async generateLoginQR(userId: string): Promise&amp;lt;{ qrId: string; qrCode:string }&amp;gt; {
// Generate unique QR session ID
const qrId = uuidv4();
// Store QR session with Redis (5 minutes expiration)
await this.redis.set(
qr_login:${qrId},
JSON.stringify({
status: 'pending',
ownerId: userId,
createdAt: new Date().toISOString(),
}),
'EX',300
);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Security First! 🛡️
&lt;/h3&gt;

&lt;p&gt;Here's how we keep the QR login secure:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Time-Limited Sessions&lt;/strong&gt;: Each QR code expires in 5 minutes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One-Time Use&lt;/strong&gt;: Once scanned, the QR code becomes invalid&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Device Verification&lt;/strong&gt;: We check if the scanning device is authorized&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-time Status Updates&lt;/strong&gt;: Using Redis for instant status changes&lt;/li&gt;
&lt;/ol&gt;

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

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

&lt;ol&gt;
&lt;li&gt;User opens SyncBridge on their computer&lt;/li&gt;
&lt;li&gt;Clicks "Add Device" and gets a unique QR code&lt;/li&gt;
&lt;li&gt;Scans with their phone (already logged in)&lt;/li&gt;
&lt;li&gt;Both devices get notified of successful pairing&lt;/li&gt;
&lt;li&gt;New device receives its own unique tokens
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Real-time QR status monitoring
const checkQRStatus = async (qrId: string) =&amp;gt; {
const status = await redis.get(qr_login:${qrId});
if (status.authenticated) {
// Handle successful authentication
const { deviceId, tokens } = status;
await setupNewDevice(deviceId, tokens);
}
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Email Verification: Double-Check Everything ✉️
&lt;/h3&gt;

&lt;p&gt;Every new device pairing triggers an email notification:&lt;/p&gt;

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

&lt;p&gt;This gives users instant visibility into their account activity and a chance to revoke access if something looks suspicious.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security Monitoring: The Guardian Angel 👀
&lt;/h2&gt;

&lt;p&gt;What happens after users log in? We keep watching! Here's our security monitoring system in action:&lt;/p&gt;

&lt;h3&gt;
  
  
  Event Tracking System 📊
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;async logSecurityEvent(params: {
user: User,
device: Device,
event_type: SecurityEventType,
severity: SecurityEventSeverity,
}) {
// Log and monitor security events
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What We Track 🔍
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;🚪 Login attempts (successful and failed)&lt;/li&gt;
&lt;li&gt;🔄 Token refresh patterns&lt;/li&gt;
&lt;li&gt;📱 Device pairing activities&lt;/li&gt;
&lt;li&gt;🌍 Geographic anomalies&lt;/li&gt;
&lt;li&gt;⚠️ Suspicious IP changes&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Putting It All Together 🎯
&lt;/h2&gt;

&lt;p&gt;Here's what makes SyncBridge's authentication special:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Zero Trust Architecture&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every request is verified&lt;/li&gt;
&lt;li&gt;No device is inherently trusted&lt;/li&gt;
&lt;li&gt;Continuous validation&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;User-First Security&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Clear email notifications&lt;/li&gt;
&lt;li&gt;Device management dashboard&lt;/li&gt;
&lt;li&gt;One-click device revocation&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Future-Proof Design&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scalable token management&lt;/li&gt;
&lt;li&gt;Extensible security events&lt;/li&gt;
&lt;li&gt;Ready for new auth methods&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;Building a secure authentication system is like building a house - you need a solid foundation (token management), good locks (QR code pairing), and a reliable security system (event monitoring). &lt;/p&gt;

&lt;p&gt;The best part? This is just the beginning. Next up: implementing biometric authentication and hardware key support! 🚀&lt;/p&gt;




&lt;p&gt;Found this helpful? Follow me for more deep dives into web security and full-stack development! &lt;br&gt;
LINKED IN : &lt;a href="https://www.linkedin.com/in/seif-eddine-jlassi/" rel="noopener noreferrer"&gt;https://www.linkedin.com/in/seif-eddine-jlassi/&lt;/a&gt;&lt;br&gt;
GITHUB REPO :&lt;a href="https://github.com/laakri/SyncBridge" rel="noopener noreferrer"&gt;https://github.com/laakri/SyncBridge&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Building SyncBridge: When "Copy Here, Paste There" Gets an Upgrade 🚀</title>
      <dc:creator>Saif Jlassi</dc:creator>
      <pubDate>Sat, 21 Dec 2024 19:23:54 +0000</pubDate>
      <link>https://dev.to/laakri/building-syncbridge-when-copy-here-paste-there-gets-an-upgrade-n4b</link>
      <guid>https://dev.to/laakri/building-syncbridge-when-copy-here-paste-there-gets-an-upgrade-n4b</guid>
      <description>&lt;p&gt;Ever had that moment when you're reading an article on your phone, but want to continue on your laptop? Or found yourself copying text between devices like a digital juggler? Yeah, me too. That's why I built SyncBridge.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  The "Aha!" Moment
&lt;/h2&gt;

&lt;p&gt;Picture this: I'm coding on my desktop, reading docs on my tablet, and messaging on my phone. Copy, switch device, paste. Switch again. Copy again. At that moment, I thought, "This is madness. Why isn't this seamless?"&lt;/p&gt;

&lt;h2&gt;
  
  
  What's SyncBridge, Anyway?
&lt;/h2&gt;

&lt;p&gt;Think of it as a digital bridge between all your devices. You're reading on your phone? Swipe up, continue on your laptop. Found a cool code snippet? It's already in your tablet's clipboard. Writing notes? They sync across devices before you even finish your thought.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  The Secret Sauce 🌶️
&lt;/h2&gt;

&lt;p&gt;What makes SyncBridge different isn't just sync – it's the experience. No more "send to device" buttons or manual syncing. It just works. Like magic, but with better error handling. 😉&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cool Stuff Under the Hood:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Real-time sync that's faster than your coffee break&lt;/li&gt;
&lt;li&gt;Smart device pairing (scan &amp;amp; go)&lt;/li&gt;
&lt;li&gt;Automatic conflict resolution (no more "which version is newer?")&lt;/li&gt;
&lt;li&gt;Battery-friendly background sync&lt;/li&gt;
&lt;li&gt;End-to-end encryption (because your data is yours)&lt;/li&gt;
&lt;/ul&gt;

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

&lt;h2&gt;
  
  
  Current Status: The Foundation
&lt;/h2&gt;

&lt;p&gt;Right now, I've laid down the architecture (shown above) and mapped out the core features. Think of it as building the highway before the cars start rolling. The diagrams show the planned user flows and technical architecture – the blueprint of what's coming.&lt;/p&gt;

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

&lt;p&gt;This is just the beginning. I'm building SyncBridge in public, and I'll be sharing every major milestone, challenge, and "why didn't I think of this before?" moment. Next up: the real-time sync engine that makes everything feel instantaneous.&lt;/p&gt;

&lt;h2&gt;
  
  
  Join the Journey
&lt;/h2&gt;

&lt;p&gt;Want to see how a "wouldn't it be cool if..." moment turns into a full-fledged product? Follow along! I'll be posting regular updates as SyncBridge grows from these architecture diagrams into your next favorite tool.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>websocket</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Building a Real-Time Auction Platform: Behind the Scenes</title>
      <dc:creator>Saif Jlassi</dc:creator>
      <pubDate>Tue, 10 Dec 2024 17:28:00 +0000</pubDate>
      <link>https://dev.to/laakri/building-a-real-time-auction-platform-behind-the-scenes-5hl6</link>
      <guid>https://dev.to/laakri/building-a-real-time-auction-platform-behind-the-scenes-5hl6</guid>
      <description>&lt;p&gt;As a developer, I recently built a sophisticated auction platform that combines the excitement of live bidding with modern social features. Today, I want to share the journey of creating the core auction experience, particularly focusing on real-time interactions.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  The Auction Experience
&lt;/h2&gt;

&lt;p&gt;Our auction platform offers a dynamic, interactive environment where users can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Watch live auctions with real-time price updates&lt;/li&gt;
&lt;li&gt;Participate in auction-specific chat rooms&lt;/li&gt;
&lt;li&gt;See live viewer counts&lt;/li&gt;
&lt;li&gt;Place bids with instant feedback&lt;/li&gt;
&lt;li&gt;Track time remaining with precision countdowns&lt;/li&gt;
&lt;li&gt;Interact with other bidders in real-time&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Key Features
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Live Bidding System
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Real-time price updates across all connected users&lt;/li&gt;
&lt;li&gt;Anti-sniping protection&lt;/li&gt;
&lt;li&gt;Minimum increment enforcement&lt;/li&gt;
&lt;li&gt;Instant bid validation&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Social Integration
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Live chat during auctions&lt;/li&gt;
&lt;li&gt;Real-time viewer count&lt;/li&gt;
&lt;li&gt;User presence indicators&lt;/li&gt;
&lt;li&gt;Follow/unfollow sellers&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Owner Controls
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Real-time auction management&lt;/li&gt;
&lt;li&gt;Chat moderation capabilities&lt;/li&gt;
&lt;li&gt;Early ending option&lt;/li&gt;
&lt;li&gt;Private auction management&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Real-Time Architecture
&lt;/h2&gt;

&lt;p&gt;The heart of our platform is the real-time communication system. Here's how we implemented it:&lt;/p&gt;

&lt;h3&gt;
  
  
  WebSocket Implementation
&lt;/h3&gt;

&lt;p&gt;We built a robust WebSocket architecture that handles:&lt;/p&gt;

&lt;h4&gt;
  
  
  1. Bidding Events
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Instant price updates&lt;/li&gt;
&lt;li&gt;Bid confirmations&lt;/li&gt;
&lt;li&gt;Outbid notifications&lt;/li&gt;
&lt;li&gt;Current price synchronization&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  2. Room Management
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Auction room joining/leaving&lt;/li&gt;
&lt;li&gt;Viewer count tracking&lt;/li&gt;
&lt;li&gt;User presence management&lt;/li&gt;
&lt;li&gt;Chat room coordination&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  3. State Synchronization
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Auction status updates&lt;/li&gt;
&lt;li&gt;Timer synchronization&lt;/li&gt;
&lt;li&gt;Owner control changes&lt;/li&gt;
&lt;li&gt;Chat state management&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Challenges &amp;amp; Solutions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Race Conditions
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Implemented server-side bid validation&lt;/li&gt;
&lt;li&gt;Used atomic operations for price updates&lt;/li&gt;
&lt;li&gt;Maintained bid order consistency&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. State Management
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Centralized state management for consistency&lt;/li&gt;
&lt;li&gt;Real-time data synchronization&lt;/li&gt;
&lt;li&gt;Optimistic UI updates with rollback&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Performance
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Efficient WebSocket message handling&lt;/li&gt;
&lt;li&gt;Minimal payload sizes&lt;/li&gt;
&lt;li&gt;Smart reconnection handling&lt;/li&gt;
&lt;/ul&gt;

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

&lt;h3&gt;
  
  
  1. Responsive Updates
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Instant UI feedback for actions&lt;/li&gt;
&lt;li&gt;Smooth animations for price changes&lt;/li&gt;
&lt;li&gt;Real-time countdown synchronization&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Error Handling
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Graceful connection loss handling&lt;/li&gt;
&lt;li&gt;Automatic reconnection&lt;/li&gt;
&lt;li&gt;State recovery after disconnection&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Security
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Bid verification&lt;/li&gt;
&lt;li&gt;User authentication&lt;/li&gt;
&lt;li&gt;Rate limiting&lt;/li&gt;
&lt;li&gt;Private auction access control&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Technical Insights
&lt;/h2&gt;

&lt;p&gt;The most challenging aspect was maintaining data consistency across all connected clients while ensuring a smooth user experience. We achieved this through:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Event-Driven Architecture
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Centralized event handling&lt;/li&gt;
&lt;li&gt;Predictable state updates&lt;/li&gt;
&lt;li&gt;Clean separation of concerns&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Real-Time Data Flow
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Bidirectional communication&lt;/li&gt;
&lt;li&gt;Event queuing&lt;/li&gt;
&lt;li&gt;State reconciliation&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Scalability Considerations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Horizontal scaling capability&lt;/li&gt;
&lt;li&gt;Connection pooling&lt;/li&gt;
&lt;li&gt;Load balancing ready&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ol&gt;
&lt;li&gt;Always handle disconnections gracefully&lt;/li&gt;
&lt;li&gt;Implement proper error boundaries&lt;/li&gt;
&lt;li&gt;Use optimistic UI updates carefully&lt;/li&gt;
&lt;li&gt;Maintain a consistent state across all clients&lt;/li&gt;
&lt;li&gt;Consider latency in user experience design&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This project showcases how modern web technologies can create engaging, real-time experiences. The combination of WebSocket communication, state management, and user experience design creates a platform that's both powerful and user-friendly.&lt;/p&gt;




&lt;h1&gt;
  
  
  #webdev #javascript #typescript #websockets #react
&lt;/h1&gt;

</description>
      <category>webdev</category>
      <category>react</category>
      <category>typescript</category>
      <category>fastify</category>
    </item>
    <item>
      <title>Building Glide: A Modern E-commerce Platform with Angular 17 and .NET 8</title>
      <dc:creator>Saif Jlassi</dc:creator>
      <pubDate>Tue, 10 Dec 2024 16:40:29 +0000</pubDate>
      <link>https://dev.to/laakri/building-glide-a-modern-e-commerce-platform-with-angular-17-and-net-8-17im</link>
      <guid>https://dev.to/laakri/building-glide-a-modern-e-commerce-platform-with-angular-17-and-net-8-17im</guid>
      <description>&lt;p&gt;Hey fellow developers! Today I'm excited to share my journey building Glide, a modern e-commerce platform that combines cutting-edge technologies with practical solutions. Whether you're a student looking to learn or a developer seeking insights, this post will walk you through the key aspects of building a full-stack e-commerce application.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  The Vision Behind Glide
&lt;/h2&gt;

&lt;p&gt;Every great project starts with a clear vision. For Glide, it was creating an e-commerce platform that not only processes transactions but delivers an exceptional user experience. The goal was to build something that both developers would appreciate for its clean architecture and users would love for its intuitive interface.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech Stack: Why These Choices Matter
&lt;/h2&gt;

&lt;p&gt;The foundation of Glide rests on two powerful frameworks:&lt;/p&gt;

&lt;p&gt;🔹 &lt;strong&gt;Frontend: Angular 17&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Standalone components for better modularity&lt;/li&gt;
&lt;li&gt;New control flow syntax for cleaner templates&lt;/li&gt;
&lt;li&gt;Built-in SSR capabilities for better SEO&lt;/li&gt;
&lt;li&gt;DaisyUI + Tailwind CSS for responsive design&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;🔹 &lt;strong&gt;Backend: .NET 8&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Robust API architecture&lt;/li&gt;
&lt;li&gt;JWT + Google OAuth integration&lt;/li&gt;
&lt;li&gt;Entity Framework Core with MySQL&lt;/li&gt;
&lt;li&gt;Real-time capabilities with SignalR&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Key Features That Make Glide Stand Out
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Authentication &amp;amp; Security
&lt;/h3&gt;

&lt;p&gt;The authentication system combines traditional JWT tokens with Google OAuth, providing users with flexible login options while maintaining robust security. The implementation includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Secure token management&lt;/li&gt;
&lt;li&gt;Role-based access control&lt;/li&gt;
&lt;li&gt;Protected API endpoints&lt;/li&gt;
&lt;li&gt;CORS policy implementation&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Real-Time Features
&lt;/h3&gt;

&lt;p&gt;Using SignalR, we've implemented:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Live notifications&lt;/li&gt;
&lt;li&gt;Order status updates&lt;/li&gt;
&lt;li&gt;Real-time cart synchronization&lt;/li&gt;
&lt;li&gt;Instant messaging between users and support&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Admin Dashboard
&lt;/h3&gt;

&lt;p&gt;A comprehensive admin interface featuring:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sales analytics with Chart.js&lt;/li&gt;
&lt;li&gt;Inventory management&lt;/li&gt;
&lt;li&gt;Order processing&lt;/li&gt;
&lt;li&gt;User management&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Development Insights &amp;amp; Best Practices
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Frontend Architecture
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Components are organized by feature&lt;/li&gt;
&lt;li&gt;Services handle state management&lt;/li&gt;
&lt;li&gt;Interceptors manage HTTP requests&lt;/li&gt;
&lt;li&gt;Guards protect restricted routes&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Backend Structure
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Clean architecture principles&lt;/li&gt;
&lt;li&gt;Repository pattern for data access&lt;/li&gt;
&lt;li&gt;Middleware for request/response handling&lt;/li&gt;
&lt;li&gt;Structured error handling&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Challenges &amp;amp; Solutions
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Performance Optimization&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Implemented lazy loading for routes&lt;/li&gt;
&lt;li&gt;Used Angular's built-in optimization tools&lt;/li&gt;
&lt;li&gt;Configured proper caching strategies&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;State Management&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Created service-based state management&lt;/li&gt;
&lt;li&gt;Implemented reactive patterns&lt;/li&gt;
&lt;li&gt;Used BehaviorSubjects for real-time updates&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;typescript&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt; // Authentication configuration
&amp;gt; builder.Services.AddAuthentication(options =&amp;gt; {
&amp;gt;     options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
&amp;gt;     options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
&amp;gt; })
&amp;gt; .AddJwtBearer()
&amp;gt; .AddGoogle(options =&amp;gt; {
&amp;gt;     options.Scope.Add("email");
&amp;gt;     options.Scope.Add("profile");
&amp;gt; });
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Learning Outcomes
&lt;/h2&gt;

&lt;p&gt;Building Glide taught valuable lessons about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Modern web architecture&lt;/li&gt;
&lt;li&gt;Security best practices&lt;/li&gt;
&lt;li&gt;Performance optimization&lt;/li&gt;
&lt;li&gt;User experience design&lt;/li&gt;
&lt;li&gt;Real-time feature implementation&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tips for Students
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Start Small&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Begin with core features&lt;/li&gt;
&lt;li&gt;Add complexity gradually&lt;/li&gt;
&lt;li&gt;Test thoroughly as you go&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Focus on Fundamentals&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Master TypeScript/C# basics&lt;/li&gt;
&lt;li&gt;Understand HTTP protocols&lt;/li&gt;
&lt;li&gt;Learn authentication concepts&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Best Practices&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Write clean, documented code&lt;/li&gt;
&lt;li&gt;Follow naming conventions&lt;/li&gt;
&lt;li&gt;Use version control effectively&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Future Roadmap
&lt;/h2&gt;

&lt;p&gt;Glide continues to evolve with planned features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AI-powered product recommendations&lt;/li&gt;
&lt;li&gt;Enhanced analytics dashboard&lt;/li&gt;
&lt;li&gt;Mobile app development&lt;/li&gt;
&lt;li&gt;Performance optimizations&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Get Involved
&lt;/h2&gt;

&lt;p&gt;The project is open source and welcomes contributions! Whether you're fixing bugs, adding features, or improving documentation, your input is valuable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources for Learning
&lt;/h2&gt;

&lt;p&gt;To build something similar, check out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Angular Documentation&lt;/li&gt;
&lt;li&gt;.NET 8 Documentation&lt;/li&gt;
&lt;li&gt;JWT Authentication guides&lt;/li&gt;
&lt;li&gt;SignalR tutorials&lt;/li&gt;
&lt;li&gt;DaisyUI components&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Connect &amp;amp; Contribute
&lt;/h2&gt;

&lt;p&gt;I believe in learning together and sharing knowledge. If you're interested in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Contributing to the project&lt;/li&gt;
&lt;li&gt;Learning more about the implementation&lt;/li&gt;
&lt;li&gt;Discussing web development&lt;/li&gt;
&lt;li&gt;Sharing your own experiences&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Feel free to reach out! You can find me on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: [Your GitHub]&lt;/li&gt;
&lt;li&gt;LinkedIn: [Your LinkedIn]&lt;/li&gt;
&lt;li&gt;Twitter: [Your Twitter]&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Remember, every developer started somewhere. Keep learning, keep building, and most importantly, enjoy the process!&lt;/p&gt;

&lt;h1&gt;
  
  
  WebDevelopment #Angular #DotNet #Programming #OpenSource #TechEducation
&lt;/h1&gt;

</description>
      <category>webdev</category>
      <category>angular</category>
      <category>dotnet</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
