<?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: Eugene Yakhnenko</title>
    <description>The latest articles on DEV Community by Eugene Yakhnenko (@eugenioenko).</description>
    <link>https://dev.to/eugenioenko</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1225296%2Ffaac6a57-1b68-4e94-a89a-c5bc28538079.jpeg</url>
      <title>DEV Community: Eugene Yakhnenko</title>
      <link>https://dev.to/eugenioenko</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/eugenioenko"/>
    <language>en</language>
    <item>
      <title>What 200 Concurrent Users Taught Me About SQLite Performance</title>
      <dc:creator>Eugene Yakhnenko</dc:creator>
      <pubDate>Tue, 28 Apr 2026 20:00:51 +0000</pubDate>
      <link>https://dev.to/eugenioenko/what-200-concurrent-users-taught-me-about-sqlite-performance-442j</link>
      <guid>https://dev.to/eugenioenko/what-200-concurrent-users-taught-me-about-sqlite-performance-442j</guid>
      <description>&lt;p&gt;I was about to release &lt;a href="http://autentico.top/" rel="noopener noreferrer"&gt;Autentico&lt;/a&gt; 2.0. The feature work was done, tests were passing, docs were updated. Before tagging the release I figured I'd spend some time on performance. Run some stress tests, see where things stand, maybe squeeze out some easy wins. What followed was a week-long detour through profiling, architecture design, benchmarking, and a humbling lesson about assumptions.&lt;/p&gt;

&lt;p&gt;Autentico is a self-contained OAuth 2.0 / OpenID Connect identity provider built with Go and SQLite. One binary, one database file, no external dependencies. The benchmark workload is a full PKCE authorization code flow: authorize, login with password, token exchange, token introspection, and refresh. Five HTTP requests per iteration, four or five SQLite writes per iteration, and one bcrypt password verification.&lt;/p&gt;

&lt;h2&gt;
  
  
  Profiling on the Wrong Machine
&lt;/h2&gt;

&lt;p&gt;I started with k6 stress tests on my older i5 laptop. 100 virtual users, 30 seconds, the full auth flow. The results were fine but not great. So I profiled.&lt;/p&gt;

&lt;p&gt;90% of CPU time was spent in &lt;code&gt;bcrypt.CompareHashAndPassword&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That's the function that verifies a user's password against the stored hash. It's intentionally slow (that's the point of bcrypt), it's CPU-bound, and it was dominating everything else. SQLite writes took microseconds. JWT signing was negligible. HTTP routing was invisible. Just bcrypt, eating all available cores.&lt;/p&gt;

&lt;p&gt;The conclusion seemed obvious: bcrypt is the bottleneck, and you can't make bcrypt faster. You can only do more of it in parallel. But on a single machine running SQLite, you can't just add more instances. SQLite is single-writer, single-file. You can't horizontally scale the traditional way.&lt;/p&gt;

&lt;p&gt;Or can you?&lt;/p&gt;

&lt;h2&gt;
  
  
  Designing Verifico
&lt;/h2&gt;

&lt;p&gt;The bottleneck wasn't the database. It was one function call. So what if you scaled just that function?&lt;/p&gt;

&lt;p&gt;I explored the options systematically:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CQRS with SQLite replication.&lt;/strong&gt; LiteFS can replicate SQLite across nodes, one primary for writes, replicas for reads. A real architecture, but it solves a general scaling problem. Mine was specific. I didn't need to distribute reads and writes. I needed to distribute bcrypt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Postgres.&lt;/strong&gt; The standard answer for outgrowing SQLite. But Postgres doesn't solve bcrypt CPU. You'd still run &lt;code&gt;CompareHashAndPassword&lt;/code&gt; on the application server. Multiple instances behind a load balancer would spread the load, but you'd be paying for full application instances (database connections, memory, middleware) when all you need is more CPU for one function.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Child processes.&lt;/strong&gt; Spawn separate processes for bcrypt work. But Go already parallelizes CPU-bound work across all cores via goroutines and the runtime scheduler. On a single machine, you can't beat Go's built-in parallelism. Separate processes just add IPC overhead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sticky sessions.&lt;/strong&gt; Route users to specific instances. But you need a shared lookup table, which needs a shared database, which is the problem you're trying to avoid.&lt;/p&gt;

&lt;p&gt;Then the idea clicked: keep Autentico as a single instance, owning the database and handling everything. But when it needs to verify a password, send the hash and the plaintext to a remote worker. The worker runs bcrypt and returns true or false. Workers are stateless, trivial, and can run on the cheapest hardware available.&lt;/p&gt;

&lt;p&gt;I called it Verifico ("I verify" in Italian, matching Autentico's naming). Same binary, new subcommand: &lt;code&gt;autentico verifico start&lt;/code&gt;. One HTTP endpoint, one function call, a shared secret for auth, and round-robin load balancing with automatic fallback to local bcrypt if workers are down.&lt;/p&gt;

&lt;p&gt;The security model went through its own journey. I started at mTLS (operationally heavy for a boolean endpoint), worked through AES encryption (reimplementing TLS poorly), landed on a shared secret over a private network. The password already traveled over the public internet to reach Autentico. One more hop inside a VPC is no worse.&lt;/p&gt;

&lt;h2&gt;
  
  
  It Worked
&lt;/h2&gt;

&lt;p&gt;On the i5, Verifico delivered real improvements. With the server constrained to 2 cores and workers handling bcrypt, non-login endpoints dropped from seconds to single-digit milliseconds. The server's cores were free for HTTP handling, SQLite queries, and JWT signing. Throughput scaled linearly with worker count, up to about 6 cores. At 8 it flattened out.&lt;/p&gt;

&lt;p&gt;I was pleased. Built a clean solution, benchmarked it, it worked. Ready to ship.&lt;/p&gt;

&lt;h2&gt;
  
  
  It Didn't Work
&lt;/h2&gt;

&lt;p&gt;Then I ran the same benchmarks on a modern Ryzen 7 desktop. 16 cores, faster single-thread performance, more cache.&lt;/p&gt;

&lt;p&gt;I constrained Autentico to 2 cores and started adding 2-core workers: 2+2, 2+2+2, all the way up to 2+7x2. On the i5, throughput had kept climbing with each worker up to 6 cores. On the Ryzen:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Config&lt;/th&gt;
&lt;th&gt;iter/s&lt;/th&gt;
&lt;th&gt;Login p95&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2 server + 2 worker&lt;/td&gt;
&lt;td&gt;15.4/s&lt;/td&gt;
&lt;td&gt;3.61s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2 server + 4 worker&lt;/td&gt;
&lt;td&gt;15.4/s&lt;/td&gt;
&lt;td&gt;3.68s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2 server + 6 worker&lt;/td&gt;
&lt;td&gt;15.2/s&lt;/td&gt;
&lt;td&gt;3.58s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2 server + 10 worker&lt;/td&gt;
&lt;td&gt;15.0/s&lt;/td&gt;
&lt;td&gt;3.60s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2 server + 14 worker&lt;/td&gt;
&lt;td&gt;14.7/s&lt;/td&gt;
&lt;td&gt;3.76s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Flat. Five configurations, 2 to 14 worker cores, and throughput barely moved. Adding workers did nothing.&lt;/p&gt;

&lt;p&gt;The Ryzen was simply faster at bcrypt. Even at the default cost of 10, each core chewed through password hashes fast enough that bcrypt stopped being the bottleneck. The real contention was elsewhere entirely.&lt;/p&gt;

&lt;p&gt;I had spent days designing, implementing, and benchmarking a solution for a bottleneck that was hardware-specific.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding the Real Bottleneck
&lt;/h2&gt;

&lt;p&gt;I went back to profiling, this time on the Ryzen. A Go block profile under load revealed that every contention point was at &lt;code&gt;database/sql.(*DB).conn&lt;/code&gt;. Goroutines waiting for a connection from the pool. Not SQLite's file lock, not disk I/O. The Go connection pool.&lt;/p&gt;

&lt;p&gt;Reads accounted for 65% of total contention, writes 35%. The top offenders were all routine operations: looking up a client by ID, creating a session, creating a token. Fast queries, stuck waiting in line.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Boring Win: WAL Mode
&lt;/h2&gt;

&lt;p&gt;SQLite's default rollback journal locks the entire database during writes, blocking all readers. WAL (Write-Ahead Logging) changes this: readers see a consistent snapshot while writes go to a separate log. The change is one line:&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="n"&gt;PRAGMA&lt;/span&gt; &lt;span class="n"&gt;journal_mode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;WAL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's persistent. Set it once and every future connection inherits it. No application code changes.&lt;/p&gt;

&lt;p&gt;Results at 200 virtual users, 30 seconds:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cores&lt;/th&gt;
&lt;th&gt;Without WAL&lt;/th&gt;
&lt;th&gt;With WAL&lt;/th&gt;
&lt;th&gt;Improvement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;13.4 iter/s&lt;/td&gt;
&lt;td&gt;16.7 iter/s&lt;/td&gt;
&lt;td&gt;+25%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;23.6 iter/s&lt;/td&gt;
&lt;td&gt;31.3 iter/s&lt;/td&gt;
&lt;td&gt;+33%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;32.2 iter/s&lt;/td&gt;
&lt;td&gt;49.8 iter/s&lt;/td&gt;
&lt;td&gt;+55%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;33.0 iter/s&lt;/td&gt;
&lt;td&gt;54.3 iter/s&lt;/td&gt;
&lt;td&gt;+65%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;31.9 iter/s&lt;/td&gt;
&lt;td&gt;50.2 iter/s&lt;/td&gt;
&lt;td&gt;+57%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One pragma. No code changes. Up to 65% throughput improvement. But WAL alone hits a ceiling around 6 cores and actually regresses past that.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Scaling Win: Read/Write Pool Split
&lt;/h2&gt;

&lt;p&gt;WAL allows concurrent readers alongside a single writer. The natural next step: give readers their own connection pool.&lt;/p&gt;

&lt;p&gt;I split the single &lt;code&gt;*sql.DB&lt;/code&gt; into two pools. A write pool with one connection (serializing all mutations, eliminating SQLITE_BUSY errors) and a read pool with multiple connections for concurrent SELECT queries.&lt;/p&gt;

&lt;p&gt;The key was making this invisible to callers. Instead of updating every file that touches the database, I wrote a &lt;code&gt;DB&lt;/code&gt; wrapper that routes by method: &lt;code&gt;Exec&lt;/code&gt; and &lt;code&gt;Begin&lt;/code&gt; go to the writer, &lt;code&gt;Query&lt;/code&gt; and &lt;code&gt;QueryRow&lt;/code&gt; go to the reader pool. Every package just calls &lt;code&gt;db.GetDB()&lt;/code&gt; and the routing happens automatically. Zero changes to business logic.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;DB&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;writer&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DB&lt;/span&gt;
    &lt;span class="n"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DB&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;DB&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&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="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;DB&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&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="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This also required some iteration. The first attempt was slower due to a bug where pooled connections weren't getting their PRAGMA settings. Once fixed:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cores&lt;/th&gt;
&lt;th&gt;WAL Only&lt;/th&gt;
&lt;th&gt;WAL + Pool Split&lt;/th&gt;
&lt;th&gt;Improvement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;49.8 iter/s&lt;/td&gt;
&lt;td&gt;57.0 iter/s&lt;/td&gt;
&lt;td&gt;+14%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;54.3 iter/s&lt;/td&gt;
&lt;td&gt;76.1 iter/s&lt;/td&gt;
&lt;td&gt;+40%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;50.2 iter/s&lt;/td&gt;
&lt;td&gt;88.3 iter/s&lt;/td&gt;
&lt;td&gt;+76%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;unlimited&lt;/td&gt;
&lt;td&gt;45.9 iter/s&lt;/td&gt;
&lt;td&gt;101.4 iter/s&lt;/td&gt;
&lt;td&gt;+121%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Where WAL alone plateaus and regresses, the pool split keeps scaling. At 500 virtual users over 60 seconds, the pool split delivered 3.5x the throughput of the main branch with 59-78% latency reduction across all endpoints. Zero errors on both configurations.&lt;/p&gt;

&lt;p&gt;The read pool sweet spot was 4 connections. More than that floods the writer with contention when all those concurrent reads finish simultaneously and try to write. The auto-calculation &lt;code&gt;min(available CPUs, 4)&lt;/code&gt; with a floor of 2 covers most cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Shipped in 2.0
&lt;/h2&gt;

&lt;p&gt;Two changes made it into the release:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WAL mode&lt;/strong&gt;, enabled by default. Free performance for every deployment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read/write connection pool split&lt;/strong&gt;, transparent to users. The server auto-tunes the read pool size based on available CPUs.&lt;/p&gt;

&lt;p&gt;Verifico didn't ship. The benchmarks on the Ryzen showed it wasn't solving a real bottleneck, so there was no reason to add the complexity. The code is there if the need ever materializes on constrained hardware, but for now it's a solution waiting for a problem.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Profiling tells the truth, but only about the machine you're sitting at.&lt;/strong&gt; I should have known better. In my early years I spent time writing x86 assembly with FASM, where you learn that certain instructions cost more clock cycles than others and that two CPUs at the same clock speed can have very different real-world performance thanks to pipeline optimizations, L1/L2/L3 cache differences, and branch prediction. I knew hardware isn't uniform. What I didn't expect was that the &lt;em&gt;scaling behavior&lt;/em&gt; would change. I assumed that if adding worker cores improved throughput on one machine, it would improve throughput on another, maybe at different absolute numbers but with the same shape. Instead, the Ryzen's faster per-core bcrypt performance shifted the bottleneck entirely. The curve wasn't the same shape at a different scale. It was a different curve.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The boring fix usually wins.&lt;/strong&gt; WAL mode is in the SQLite documentation. Connection pooling is a well-understood pattern. Together they more than doubled throughput. Neither required novel architecture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build the optimization, then question it.&lt;/strong&gt; I don't regret building Verifico. The design process (working through CQRS, Postgres, gRPC, mTLS, landing on the simplest thing) was valuable, and it works for its intended use case. But I should have validated the assumption on more than one machine before committing to it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't benchmark at low concurrency and call it done.&lt;/strong&gt; Some of the intermediate results at 100 virtual users looked promising for approaches that fell apart at 200. Always test at your target load.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;&lt;a href="https://github.com/eugenioenko/autentico" rel="noopener noreferrer"&gt;Autentico&lt;/a&gt; is an open-source OAuth 2.0 / OpenID Connect identity provider. Version 2.0 is coming soon.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>sqlite</category>
      <category>go</category>
      <category>idp</category>
      <category>performance</category>
    </item>
    <item>
      <title>Why your drawing app uses 2% CPU when you're not using it</title>
      <dc:creator>Eugene Yakhnenko</dc:creator>
      <pubDate>Sun, 19 Apr 2026 18:45:14 +0000</pubDate>
      <link>https://dev.to/eugenioenko/why-your-drawing-app-uses-2-cpu-when-youre-not-using-it-10e0</link>
      <guid>https://dev.to/eugenioenko/why-your-drawing-app-uses-2-cpu-when-youre-not-using-it-10e0</guid>
      <description>&lt;p&gt;&lt;em&gt;A measured comparison of Figma, tldraw, Excalidraw, and Skedoodle, and the architectural choice that makes the difference.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Open your browser. Go to any drawing or whiteboarding app: tldraw, Excalidraw, Figma, whatever you use. Put it on a blank canvas. Don't touch anything.&lt;/p&gt;

&lt;p&gt;Open your browser's task manager.&lt;/p&gt;

&lt;p&gt;That app is probably using &lt;strong&gt;1–3% CPU&lt;/strong&gt; right now. Not the browser as a whole. Not all your tabs combined. Just that one page, sitting there, doing nothing visible. Figma alone burns 3.49%. Multiply across every "modern web app" tab you keep open and you start to understand why your fan spins up when you're not using the computer.&lt;/p&gt;

&lt;p&gt;I wanted to know where that CPU was going. I built a Playwright rig, loaded tldraw, Excalidraw, and Figma on a blank canvas, and sampled CPU for 30 seconds across 5 runs. I also measured a drawing app of my own, &lt;a href="https://skedoodle.top" rel="noopener noreferrer"&gt;Skedoodle&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Here's the result:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F53ip50z8bjdrsot1n4mv.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%2F53ip50z8bjdrsot1n4mv.png" alt=" " width="800" height="322"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three apps pay a tax. One sits at the measurement noise floor. This post is about where each one's idle CPU goes, and then about a more surprising finding underneath: rendering architecture isn't what determines active CPU.&lt;/p&gt;




&lt;h2&gt;
  
  
  Methodology in one paragraph
&lt;/h2&gt;

&lt;p&gt;I built a small &lt;a href="https://github.com/eugenioenko/skedoodle/tree/chore/perf-plan/perf" rel="noopener noreferrer"&gt;Playwright-based perf framework&lt;/a&gt; that opens each app in Chromium, sits on a blank canvas for 30 seconds, and samples Chrome DevTools Protocol &lt;code&gt;Performance.metrics&lt;/code&gt; every 500ms. The reported "CPU%" is &lt;code&gt;ΔTaskDuration / wall_clock&lt;/code&gt;, attributed to the &lt;strong&gt;page&lt;/strong&gt;, not the whole browser process. Median of 5 runs; whiskers on the chart are min–max. Machine: Microsoft Surface, Intel Core i5-1035G7, 8 cores, Arch Linux. The &lt;a href="https://github.com/eugenioenko/skedoodle/blob/chore/perf-plan/perf_results.md" rel="noopener noreferrer"&gt;full methodology and raw data&lt;/a&gt; are in the repo. &lt;code&gt;pnpm --filter skedoodle-perf baseline&lt;/code&gt; reproduces every number in this post.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why tldraw ticks every frame
&lt;/h2&gt;

&lt;p&gt;tldraw ships a component called &lt;code&gt;TickManager&lt;/code&gt;. It does what the name suggests: runs forever. &lt;a href="https://github.com/tldraw/tldraw/blob/main/packages/editor/src/lib/editor/managers/TickManager/TickManager.ts" rel="noopener noreferrer"&gt;Here's the relevant code&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isPaused&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cancelRaf&lt;/span&gt;&lt;span class="p"&gt;?.()&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cancelRaf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;throttleToNextFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tick&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;bind&lt;/span&gt;
&lt;span class="nf"&gt;tick&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isPaused&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;elapsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;editor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updatePointerVelocity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;elapsed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;editor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;frame&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;elapsed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;editor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tick&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;elapsed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cancelRaf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;throttleToNextFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tick&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// re-arm for next frame&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every frame (60Hz), &lt;code&gt;tick&lt;/code&gt; runs and re-schedules itself via &lt;code&gt;requestAnimationFrame&lt;/code&gt;. No dirty-flag guard. It ticks regardless of whether anything changed.&lt;/p&gt;

&lt;p&gt;And the tick does real work. It updates pointer velocity even when the pointer hasn't moved. It drains an event queue that might be empty. It fires &lt;code&gt;'frame'&lt;/code&gt; and &lt;code&gt;'tick'&lt;/code&gt; to anyone listening. The listeners do their own work: viewport and camera animation checks, scribble handlers (no-op when idle, but still a function call), and a &lt;code&gt;PerformanceManager._onFrame&lt;/code&gt; that computes &lt;code&gt;getCulledShapes()&lt;/code&gt; on every frame.&lt;/p&gt;

&lt;p&gt;None of that is wasted effort inside tldraw's model. Pointer velocity enables gesture recognition and flick handling. Frame events drive camera tweens and smooth zoom-to-fit. Culling keeps active-draw fast at scale. If you want those features, something has to run the tick. Multiply 60 ticks by half a millisecond of work each and you get ~1.5% CPU as the price of having them.&lt;/p&gt;

&lt;p&gt;Skedoodle doesn't have most of those features. So it doesn't tick.&lt;/p&gt;




&lt;h2&gt;
  
  
  Excalidraw's React reconciler
&lt;/h2&gt;

&lt;p&gt;Excalidraw does &lt;strong&gt;not&lt;/strong&gt; run a perpetual rAF. Their &lt;code&gt;throttleRAF&lt;/code&gt; helper is pull-based; it only schedules when called:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;throttleRAF&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;:&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;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="na"&gt;timerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="na"&gt;lastArgs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scheduleFunc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;timerId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&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="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;timerId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lastArgs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;lastArgs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;if &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="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;fn&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="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And their &lt;a href="https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/renderer/animation.ts" rel="noopener noreferrer"&gt;&lt;code&gt;AnimationController&lt;/code&gt;&lt;/a&gt; explicitly stops itself when there's nothing left to animate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;AnimationController&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;animations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;AnimationController&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isRunning&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// loop stops here when idle&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So where does Excalidraw's 1.18% idle CPU go? Into React's reconciler. Excalidraw stores hover state, current tool, and pointer position in React component state. Each &lt;code&gt;setState&lt;/code&gt; triggers &lt;code&gt;componentDidUpdate&lt;/code&gt;, which runs a ~160-line prev/next diff, commits to its store, fires &lt;code&gt;onChange&lt;/code&gt; listeners, and toggles theme classes.&lt;/p&gt;

&lt;p&gt;This is a reasonable design choice. Keeping interaction state in React gives you the normal React ergonomics: declarative rendering, hooks, standard event handling. The cost is that any internal state change wakes the reconciler, and at idle there's still enough internal churn (hover ticks, mouse-move handlers, periodic state sync) to keep it awake on a steady cadence.&lt;/p&gt;

&lt;p&gt;It's a different shape of problem from tldraw's: not a perpetual rAF, but a steady drip of React work, landing at roughly the same cost.&lt;/p&gt;




&lt;h2&gt;
  
  
  Figma is a different kind of cost
&lt;/h2&gt;

&lt;p&gt;Figma's idle CPU is the highest of the four, and most of it isn't rendering. At idle, the page is running: a websocket keepalive to Figma's backend, CRDT bookkeeping for the file you have open, cursor-presence logic for other collaborators, autosave timers, and the usual authenticated-product chatter (telemetry, experiment assignment, analytics).&lt;/p&gt;

&lt;p&gt;The perf runs for the other three apps (tldraw OSS, Excalidraw, Skedoodle) were local and unauthenticated. None of them were paying for collab infrastructure at measurement time. Figma doesn't offer a local-only mode, so its number reflects a shipping collaborative product rather than a fair architectural peer. Keep it in the chart as a ceiling on "what a production collaborative whiteboard costs idle," not as a comparison against the other three.&lt;/p&gt;

&lt;p&gt;The rest of this post is about the other three.&lt;/p&gt;




&lt;h2&gt;
  
  
  Skedoodle's 0.09%: event-driven rendering
&lt;/h2&gt;

&lt;p&gt;Skedoodle's idle CPU is near zero because nothing polls. The canvas doesn't repaint unless a user event changed the scene. State mutations don't wake a reconciler because canvas state isn't in React. There's no equivalent of tldraw's &lt;code&gt;TickManager&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two choices enforce this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The renderer's internal loop is disabled.&lt;/strong&gt; Skedoodle is built on &lt;a href="https://two.js.org" rel="noopener noreferrer"&gt;Two.js&lt;/a&gt;, a thin 2D renderer. Two.js's default is its own internal &lt;code&gt;requestAnimationFrame&lt;/code&gt; loop, the &lt;code&gt;autostart: true&lt;/code&gt; option. Enable it, and Two.js will call &lt;code&gt;update()&lt;/code&gt; every frame for you, forever. If it were left on, Skedoodle would measure about the same as tldraw.&lt;/p&gt;

&lt;p&gt;The first line of Skedoodle's canvas setup turns it off. &lt;a href="https://github.com/eugenioenko/skedoodle/blob/main/client/src/canvas/canvas.hook.tsx" rel="noopener noreferrer"&gt;&lt;code&gt;client/src/canvas/canvas.hook.tsx&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Two&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;autostart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;fitted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientHeight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;twoType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;appendTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;autostart: false&lt;/code&gt;, Two.js never calls &lt;code&gt;update()&lt;/code&gt; on its own. Something in the application has to call it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The application only calls &lt;code&gt;update()&lt;/code&gt; on user events.&lt;/strong&gt; Skedoodle's entire render-scheduling layer is this one method on the canvas manager:&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="nx"&gt;throttledTwoUpdate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;updateFrequency&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useOptionsStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getState&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;updateFrequency&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;updateFrequency&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;two&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;update&lt;/span&gt;&lt;span class="p"&gt;?.();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_throttledUpdate&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_lastFrequency&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;updateFrequency&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_lastFrequency&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;updateFrequency&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_throttledUpdate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;throttle&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;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;two&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;update&lt;/span&gt;&lt;span class="p"&gt;?.();&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;updateFrequency&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_throttledUpdate&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;Three things to notice.&lt;/p&gt;

&lt;p&gt;First, the function reads &lt;code&gt;updateFrequency&lt;/code&gt; from a Zustand store on every call. That's deliberate: the user can change the throttle rate from the UI at runtime, and the next invocation picks up the new value without re-instantiation.&lt;/p&gt;

&lt;p&gt;Second, if &lt;code&gt;updateFrequency&lt;/code&gt; is &lt;code&gt;0&lt;/code&gt;, the call goes through immediately with no throttling. This is "High Performance" mode in the UI. For interactions like dragging a single shape or editing a bezier handle, unthrottled gives the most responsive feel and costs nothing extra, because the call rate is already bounded by the pointer event rate.&lt;/p&gt;

&lt;p&gt;Third, for any non-zero frequency, Skedoodle builds a throttled wrapper using lodash's &lt;code&gt;throttle&lt;/code&gt; (leading + trailing edges) and caches it. The cache is keyed on the frequency value, so changing the throttle rate invalidates and rebuilds; otherwise the same wrapper is reused across calls.&lt;/p&gt;

&lt;p&gt;Who calls this? Tool handlers do, after they've mutated scene state — the brush tool on every pointer-move, the shape tool after adjusting dimensions, the pointer tool on selection changes. Zustand store mutations that affect scene state call it. Nothing else. When the user is sitting still, &lt;code&gt;throttledTwoUpdate()&lt;/code&gt; isn't called, &lt;code&gt;two.update()&lt;/code&gt; doesn't run, and the canvas doesn't repaint.&lt;/p&gt;

&lt;p&gt;That's the 0.09%. It isn't a trick. It's what's left when you remove everything that was polling.&lt;/p&gt;

&lt;p&gt;The throttle rate (10, 30, 60, or 120 FPS, or "High Performance" for unthrottled) is exposed in the Settings panel as "Update Frequency." That last detail matters: it's evidence that the event-driven model is product surface, not accidental. A thick-library architecture couldn't offer that knob, because the library owns its own tick rate.&lt;/p&gt;




&lt;h2&gt;
  
  
  The tie that's the real story
&lt;/h2&gt;

&lt;p&gt;Here's what happens when everyone's actually drawing: a synthesized 15-second pointer trace (Archimedean spiral, 60 Hz, 902 events) replayed identically across all four apps.&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%2F29rzvhu8taas6s2l7gcq.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%2F29rzvhu8taas6s2l7gcq.png" alt=" " width="800" height="322"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Skedoodle and tldraw land &lt;strong&gt;0.23 percentage points apart&lt;/strong&gt; on median CPU across five runs. That's noise-floor territory, and it's the finding that most changed how I think about this.&lt;/p&gt;

&lt;p&gt;These two apps have nothing architecturally in common on the rendering side. Skedoodle uses Two.js's SVG renderer. tldraw built its own React-plus-canvas rendering stack from scratch. Totally different choices. Same cost.&lt;/p&gt;

&lt;p&gt;Which means &lt;strong&gt;active-draw CPU is not determined by which rendering library you pick&lt;/strong&gt;. It's determined by whether your app does anything &lt;em&gt;else&lt;/em&gt; while rendering. tldraw ticks every frame and drains the queue; Skedoodle runs its throttled update. Both do roughly the same amount of shape-drawing work per user event. Same number.&lt;/p&gt;

&lt;p&gt;Excalidraw is ~8 points higher, almost certainly rough.js doing stroke roughening on every pointer event. Figma saturates a CPU core: every stroke routes through a WASM renderer, a CRDT, autosave persistence, and telemetry. Different cost structure entirely.&lt;/p&gt;

&lt;p&gt;Put together: &lt;strong&gt;idle cost and active cost are different problems.&lt;/strong&gt; Idle is about what your app does when no one's asking it to do anything, which is architectural. Active is about how much work each user interaction triggers, which is workload-dependent. The first is a design choice. The second is mostly inherent.&lt;/p&gt;

&lt;p&gt;Picking Two.js over tldraw won't make drawing faster. Picking an event-driven architecture over a perpetual tick will make your app disappear when no one's using it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The tax
&lt;/h2&gt;

&lt;p&gt;There's a reason most drawing apps use something thicker than Two.js. With a thin renderer you write your own interaction layer: selection, hit-testing, handles, undo/redo, snapping, the whole surface. In Skedoodle's case that's roughly 5,000 lines of application code that exists specifically because the library didn't provide it. tldraw, Fabric, and Konva give you all of that.&lt;/p&gt;

&lt;p&gt;Other costs worth being honest about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You re-discover bug classes.&lt;/strong&gt; An early version of Skedoodle had a selection/hover layering bug where selection chrome rendered beneath newly drawn shapes; a mature transformer library would have prevented it by owning the chrome layer. Similar categories (pointer capture during fast drags, z-ordering of rotation handles against content, hit-testing that treats stroked paths as fills, coordinate math that breaks at extreme zoom levels) are things a library like tldraw or Konva has already worked through. With a thin renderer, you encounter them yourself, usually after they ship.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Upgrade path is closer to the metal.&lt;/strong&gt; When the library has a bug, it's more likely to be your problem.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The flip side of "own your render loop" is "own your interaction stack." It's not free, just moved.&lt;/p&gt;




&lt;h2&gt;
  
  
  When not to do this
&lt;/h2&gt;

&lt;p&gt;Three workloads where event-driven rendering stops helping:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scenes that legitimately need every frame.&lt;/strong&gt; Tween systems, physics, particle effects, animated cursors. If the scene changes without user input, an event-driven loop has nothing to trigger it. You need a tick. tldraw's model fits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Thousands of continuously moving shapes.&lt;/strong&gt; When redraws are expensive &lt;em&gt;and&lt;/em&gt; frequent, the cost isn't in whether to call &lt;code&gt;update()&lt;/code&gt;, it's in whether the renderer can batch, dirty-rect, or cull. Thin renderers without those primitives stop helping.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transformer-heavy interaction surfaces.&lt;/strong&gt; If your product is defined by multi-select, rotation handles, and snapping across transformed groups, the LOC cost of building that yourself is large and front-loaded. tldraw's transformer is legitimately good. Buy it; don't rebuild it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Skedoodle's workload is sparse updates driven by user input. Event-driven fits. If it were a particle simulator, I'd want every frame to fire and I'd run a &lt;code&gt;TickManager&lt;/code&gt; too.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/eugenioenko/skedoodle/tree/chore/perf-plan/perf" rel="noopener noreferrer"&gt;perf framework&lt;/a&gt; is committed alongside the Skedoodle source, with a written-down methodology and a 5-run baseline. &lt;code&gt;pnpm install &amp;amp;&amp;amp; pnpm --filter skedoodle-perf baseline&lt;/code&gt; reproduces every number in this post. Figma needs a one-time auth capture, and the &lt;a href="https://github.com/eugenioenko/skedoodle/blob/chore/perf-plan/perf/README.md" rel="noopener noreferrer"&gt;README&lt;/a&gt; walks through it.&lt;/p&gt;

&lt;p&gt;The architectural choice here is event-driven rendering: nothing polls, renders happen because something changed. &lt;code&gt;autostart: false&lt;/code&gt; enforces it at the Two.js boundary. &lt;code&gt;throttledTwoUpdate&lt;/code&gt; enforces it inside the application. Neither alone is the whole story; the combination is.&lt;/p&gt;

&lt;p&gt;Your drawing app doesn't have to use 2% CPU when you're not using it. It uses that much because of a choice.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Source: &lt;a href="https://github.com/eugenioenko/skedoodle" rel="noopener noreferrer"&gt;github.com/eugenioenko/skedoodle&lt;/a&gt;. The perf framework lives in the &lt;code&gt;perf/&lt;/code&gt; directory; baseline numbers in &lt;code&gt;perf_results.md&lt;/code&gt;; the research notes that became this post in &lt;code&gt;article_notes.md&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>performance</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>architecture</category>
    </item>
    <item>
      <title>I Found 5 Security Bugs in My OAuth2 Provider on My First Try (With an MCP Security Tool)</title>
      <dc:creator>Eugene Yakhnenko</dc:creator>
      <pubDate>Thu, 09 Apr 2026 04:51:02 +0000</pubDate>
      <link>https://dev.to/eugenioenko/i-found-5-security-bugs-in-my-oauth2-provider-on-my-first-try-with-an-mcp-security-tool-3ckn</link>
      <guid>https://dev.to/eugenioenko/i-found-5-security-bugs-in-my-oauth2-provider-on-my-first-try-with-an-mcp-security-tool-3ckn</guid>
      <description>&lt;p&gt;I built &lt;a href="https://github.com/eugenioenko/autentico" rel="noopener noreferrer"&gt;Autentico&lt;/a&gt;, a self-contained OAuth 2.0 / OpenID Connect identity provider in Go. I took spec compliance seriously. Every code path is annotated with the RFC section it implements, I passed the OpenID Foundation conformance suite, and I ran OWASP ZAP scans against it. I thought I was in good shape.&lt;/p&gt;

&lt;p&gt;Then I connected &lt;a href="https://github.com/go-appsec/toolbox" rel="noopener noreferrer"&gt;go-appsec/toolbox&lt;/a&gt; to Claude Code, browsed my app for ten minutes, and found five vulnerabilities (including a HIGH severity issue) on my very first session with the tool. I had almost no prior experience with security testing.&lt;/p&gt;

&lt;p&gt;Here's how that happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  The foundation: RFC annotations and conformance testing
&lt;/h2&gt;

&lt;p&gt;When I built Autentico, I wanted to do things by the book. Every return path, every validation check, every error response references the exact spec section that mandates it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// RFC 7009 §2.1: "The authorization server first validates the client&lt;/span&gt;
&lt;span class="c"&gt;// credentials (in case of a confidential client)."&lt;/span&gt;
&lt;span class="n"&gt;authenticatedClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AuthenticateClientFromRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;// RFC 6749 §10.4: refresh token MUST be bound to the client it was issued to;&lt;/span&gt;
&lt;span class="c"&gt;// presenting a refresh token issued to a different client MUST be rejected.&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;authToken&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClientID&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClientID&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;authToken&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClientID&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClientID&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

&lt;span class="c"&gt;// RFC 7662 §2.2: REQUIRED. Whether the token is currently active.&lt;/span&gt;
&lt;span class="n"&gt;Active&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="s"&gt;`json:"active"`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I reviewed 10 RFCs and specs across the OAuth2 and OIDC ecosystem, tracking every MUST, SHOULD, and MAY requirement in compliance tables. I ran the OpenID Foundation conformance suite (&lt;code&gt;oidcc-basic-certification-test-plan&lt;/code&gt;) and passed. I had unit tests, e2e tests, functional tests, and browser tests.&lt;/p&gt;

&lt;p&gt;This gave me confidence in the spec compliance of the implementation. But spec compliance and security are not the same thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Traditional scanning: OWASP ZAP
&lt;/h2&gt;

&lt;p&gt;I ran an OWASP ZAP API scan (both authenticated and unauthenticated) against 169 URLs. The results were useful but shallow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Missing OWASP security headers (X-Frame-Options, CSP, Permissions-Policy, etc.)&lt;/li&gt;
&lt;li&gt;A couple of endpoints returning 500 instead of 404 for nonexistent resources&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I fixed everything in one PR. Final ZAP results: &lt;strong&gt;0 FAIL, 112 PASS, 4 WARN&lt;/strong&gt; (all informational). Clean bill of health from the scanner.&lt;/p&gt;

&lt;p&gt;ZAP tests what it can see from the outside: headers, status codes, common injection patterns. It doesn't understand OAuth flows, MFA logic, or token lifecycle. For that, I needed something different.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enter go-appsec/toolbox
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/go-appsec/toolbox" rel="noopener noreferrer"&gt;go-appsec/toolbox&lt;/a&gt; is an MCP (Model Context Protocol) server designed for collaborative security testing between humans and AI agents. It's not a scanner; it's a workbench. The idea is simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You&lt;/strong&gt; handle the browser: log in, navigate the app, trigger the flows you want tested&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The AI agent&lt;/strong&gt; watches the traffic through a proxy, analyzes it, and suggests or executes attacks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tool provides MCP tools for traffic capture (&lt;code&gt;proxy_poll&lt;/code&gt;), request replay with modifications (&lt;code&gt;replay_send&lt;/code&gt;), JWT inspection (&lt;code&gt;jwt_decode&lt;/code&gt;), cookie analysis (&lt;code&gt;cookie_jar&lt;/code&gt;), out-of-band testing (&lt;code&gt;oast_create&lt;/code&gt;), and more. You connect it to Claude Code (or any MCP-compatible client), and the AI agent uses these tools to probe your application while you drive the browser.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setup
&lt;/h3&gt;

&lt;p&gt;The setup took minutes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Start the toolbox MCP server with proxy on port 8080&lt;/li&gt;
&lt;li&gt;Configure the browser to proxy through it&lt;/li&gt;
&lt;li&gt;Connect the MCP server to Claude Code via &lt;code&gt;claude mcp add&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Browse the application to capture traffic&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I captured about 112 proxy flows covering OAuth authorization, token exchange, admin CRUD, account management, and MFA enrollment. Then I asked Claude to start testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I found: 5 vulnerabilities on my first try
&lt;/h2&gt;

&lt;p&gt;I want to emphasize: this was my first time using the tool. I had no prior pentesting experience and very little knowledge of how to use go-appsec/toolbox effectively. I was learning the workflow as I went. Despite that, the collaboration between the tool and the AI agent produced real, actionable findings.&lt;/p&gt;

&lt;h3&gt;
  
  
  The standout: unauthenticated token introspection (HIGH)
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;/oauth2/introspect&lt;/code&gt; endpoint returned full token metadata (active status, scopes, user ID, and claims) without requiring any client credentials. Anyone who had a token value could check whether it was active and extract its claims.&lt;/p&gt;

&lt;p&gt;The AI agent found this by using &lt;code&gt;request_send&lt;/code&gt; to POST to the introspect endpoint with no authorization header. The response came back &lt;code&gt;200 OK&lt;/code&gt; with &lt;code&gt;active: true&lt;/code&gt; and full claim data. This is the kind of finding that demonstrates the tool's workflow: it captured the legitimate introspect request during browsing, stripped the credentials, replayed it, and confirmed the server didn't enforce authentication. Fixed within minutes during the same session.&lt;/p&gt;

&lt;h3&gt;
  
  
  The other four
&lt;/h3&gt;

&lt;p&gt;The remaining findings were two MEDIUM and two LOW severity issues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PKCE not enforced for public clients.&lt;/strong&gt; The agent used &lt;code&gt;replay_send&lt;/code&gt; on a captured authorize flow with &lt;code&gt;code_challenge&lt;/code&gt; removed. The server accepted it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refresh tokens not rotated on use.&lt;/strong&gt; The agent hit the token endpoint twice with the same refresh token. Both succeeded.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CSRF error leaked internal config.&lt;/strong&gt; A POST without the CSRF cookie returned the environment variable name and value in the error message.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stored XSS in client_name&lt;/strong&gt; (no exploitable render context). A &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag was accepted in the admin API, though the output was HTML-encoded.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What passed (23 tests)
&lt;/h3&gt;

&lt;p&gt;Importantly, the tool also confirmed a lot of things were solid: redirect URI validation (6 bypass variants attempted), JWT &lt;code&gt;alg:none&lt;/code&gt; confusion, scope escalation, admin authorization enforcement, username enumeration timing, SQL injection, mass assignment, and account lockout logic. All held up.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the author found: 10 more issues, deeper logic bugs
&lt;/h2&gt;

&lt;p&gt;After I shared my experience, the toolbox author ran their own session against Autentico. With deeper knowledge of both the tool and security testing methodology, they found five additional vulnerabilities. All logic-level bugs that require understanding how OAuth and MFA flows interact:&lt;/p&gt;

&lt;h3&gt;
  
  
  MFA enforcement bypass (#172)
&lt;/h3&gt;

&lt;p&gt;This one is the best example of what AI-assisted testing can find that scanners can't. MFA enforcement had four independent gaps that reinforced each other:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The password grant issued tokens without any MFA challenge, even when &lt;code&gt;require_mfa&lt;/code&gt; was enabled&lt;/li&gt;
&lt;li&gt;Pre-MFA sessions weren't invalidated when the policy changed&lt;/li&gt;
&lt;li&gt;An attacker with a bearer token could rotate a user's TOTP secret without presenting a valid OTP code&lt;/li&gt;
&lt;li&gt;MFA could be disabled with just the account password, no TOTP code required&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No single gap is obvious in isolation. Finding them requires reasoning about the interaction between authentication flows, token grants, and policy enforcement. A scanner sees endpoints; the AI agent understood the MFA lifecycle.&lt;/p&gt;

&lt;h3&gt;
  
  
  Password grant authenticating deactivated users (#174)
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;AuthenticateUser()&lt;/code&gt; function didn't check &lt;code&gt;deactivated_at&lt;/code&gt;, while every other user lookup in the codebase did. A soft-deleted user could authenticate via the password grant and receive fresh tokens indefinitely. The admin who deleted the user would have no idea. This is a one-line fix (&lt;code&gt;AND deactivated_at IS NULL&lt;/code&gt;) but finding it requires noticing the inconsistency across query patterns.&lt;/p&gt;

&lt;h3&gt;
  
  
  Admin API audience validation bypass (#183)
&lt;/h3&gt;

&lt;p&gt;The admin API only checked that the user had the admin role. Any token belonging to an admin user was accepted regardless of which client issued it. A malicious app registered with the IdP could trick an admin into authorizing it, then replay that token against the admin API for full control. The fix enforces that tokens must also include admin audience in their audience claim, which only tokens issued through the admin client carry by default.&lt;/p&gt;

&lt;h3&gt;
  
  
  The other ones:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Empty &lt;code&gt;aud&lt;/code&gt; claim in access tokens (#171).&lt;/strong&gt; Tokens had &lt;code&gt;"aud": []&lt;/code&gt;, and the admin middleware didn't validate &lt;code&gt;azp&lt;/code&gt;, so a token from any client worked on the admin API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing &lt;code&gt;Cache-Control: no-store&lt;/code&gt; headers (#173).&lt;/strong&gt; Sensitive API responses (user lists, settings, sessions) could be cached by browsers and proxies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blind SSRF in federation discovery (#177).&lt;/strong&gt; The HTTP client followed redirects to internal/loopback addresses when fetching federated IdP discovery documents.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;I tested my OAuth2 provider with three approaches:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;What it found&lt;/th&gt;
&lt;th&gt;Depth&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OIDC Conformance Suite&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Spec compliance gaps&lt;/td&gt;
&lt;td&gt;Protocol-level&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OWASP ZAP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Missing headers, error handling&lt;/td&gt;
&lt;td&gt;Surface-level&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;go-appsec/toolbox + AI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;10 vulnerabilities including auth bypass, MFA gaps, SSRF&lt;/td&gt;
&lt;td&gt;Logic-level&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The traditional tools did their job. They confirmed my implementation followed the specs and had standard security headers in place. But the logic-level vulnerabilities (the ones that actually matter for an identity provider) only surfaced when an AI agent could reason about how the pieces fit together.&lt;/p&gt;

&lt;p&gt;What surprised me most is that I didn't need to be a security expert to get value from this. The MCP collaboration model means the agent brings security testing knowledge and methodology, while you bring the application context (which flows matter, what the admin UI does, how MFA is supposed to work). Together, you cover ground that neither could alone.&lt;/p&gt;

&lt;p&gt;Ten minutes of browsing. First time using the tool. Five findings, three fixed on the spot. That's a pretty compelling return on investment for any developer who cares about the security of what they're building.&lt;/p&gt;

&lt;p&gt;All 10 findings across both sessions have been fixed and are tracked in the &lt;a href="https://github.com/eugenioenko/autentico" rel="noopener noreferrer"&gt;Autentico Github&lt;/a&gt;. All thanks to &lt;a href="https://github.com/go-appsec/toolbox" rel="noopener noreferrer"&gt;go-appsec/toolbox Github&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>idp</category>
      <category>security</category>
      <category>ai</category>
      <category>mcp</category>
    </item>
    <item>
      <title>Why I Built an Identity Provider in Go and SQLite</title>
      <dc:creator>Eugene Yakhnenko</dc:creator>
      <pubDate>Thu, 26 Mar 2026 23:44:40 +0000</pubDate>
      <link>https://dev.to/eugenioenko/zero-ceremony-identity-why-i-built-a-single-binary-oidc-provider-in-go-12d1</link>
      <guid>https://dev.to/eugenioenko/zero-ceremony-identity-why-i-built-a-single-binary-oidc-provider-in-go-12d1</guid>
      <description>&lt;p&gt;When I set out to build &lt;a href="https://autentico.top/" rel="noopener noreferrer"&gt;Auténtico&lt;/a&gt;, my primary goal was to create a fully-featured OpenID Connect Identity Provider where &lt;strong&gt;operational simplicity was the first-class design principle&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Identity infrastructure is notoriously complex. A typical self-hosted setup involves a database server, a cache tier like Redis, a worker queue, and the identity service itself. When I needed a lightweight OpenID Connect (OIDC) server to run on a small 2GB RAM VPS, I realized the existing landscape was either operationally exhausting or structurally flawed for my specific needs.&lt;/p&gt;

&lt;p&gt;This is the story of how (and why) I built &lt;strong&gt;Auténtico&lt;/strong&gt;, a self-contained, single-binary OIDC provider backed by SQLite that removes the ceremony from identity management.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Itch: Finding the Right Lightweight IdP
&lt;/h2&gt;

&lt;p&gt;My journey started because I was researching and implementing a frontend OIDC library for product needs at my company. That scratched an itch, and I evolved it into a functional backend OIDC protocol server in Go.&lt;/p&gt;

&lt;p&gt;Months later, when I needed a lightweight Identity Provider, I evaluated the popular options but quickly hit roadblocks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Casdoor:&lt;/strong&gt; I didn't like how they treated private data. Their demo instances recycle accounts every 5 minutes, making it impossible to truly test account deletion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PocketId:&lt;/strong&gt; This is a fantastic tool, but it had a critical UX flaw for my needs: it is &lt;strong&gt;passkey-only&lt;/strong&gt; by default.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;While passkeys are the future, the current ecosystem is heavily fragmented. If a user is on an older OS or a restrictive browser, a passkey-only IdP completely locks them out. &lt;/p&gt;




&lt;h2&gt;
  
  
  The Antidote: Zero-Ceremony Architecture
&lt;/h2&gt;

&lt;p&gt;I decided to convert my OIDC protocol server into a full IdP, ensuring that every architectural decision was evaluated against a single question: &lt;strong&gt;does this reduce or increase the operational burden on the person running this?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://autentico.top/" rel="noopener noreferrer"&gt;Auténtico&lt;/a&gt; removes the entire traditional identity stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Single Binary:&lt;/strong&gt; The entire IdP runs as one Go binary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embedded SQLite:&lt;/strong&gt; There is no separate database server. The entire state lives in one file. Eliminating the external database removes connection pool tuning, credential rotation, and network partitions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No External Infrastructure:&lt;/strong&gt; No Redis, no Postgres, no message queues. Background cleanup goroutines automatically purge expired tokens, sessions, and auth codes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embedded UIs:&lt;/strong&gt; Both the Admin dashboard (React/Ant Design) and the user-facing Account UI (React/Tailwind) are compiled directly into the binary using &lt;code&gt;go:embed&lt;/code&gt;. There are zero separate frontend deployments.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Flexibility Over Dogma: Solving the Passkey Trap
&lt;/h2&gt;

&lt;p&gt;To solve the hardware and OS fragmentation issues I experienced with passkeys, I ensured Auténtico wouldn't trap operators into a single authentication path.&lt;/p&gt;

&lt;p&gt;Instead, Auténtico offers &lt;strong&gt;three distinct authentication modes&lt;/strong&gt; that are switchable at runtime without restarting the server:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;code&gt;password&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;password_and_passkey&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;passkey_only&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you deploy &lt;code&gt;passkey_only&lt;/code&gt; and discover your users' specific browser combinations are failing, you can instantly flip a setting in the Admin UI to fall back to passwords. For robust security without passkeys, it includes standard fallback methods like &lt;strong&gt;TOTP&lt;/strong&gt; (with in-browser QR enrollment) and &lt;strong&gt;Email OTP&lt;/strong&gt;. For users with modern browsers, it fully supports hardware-backed &lt;strong&gt;FIDO2&lt;/strong&gt; authentication and even allows first-login registration in one seamless flow.&lt;/p&gt;




&lt;h2&gt;
  
  
  The "Deliberately Un-clever" Architecture &amp;amp; The AI Accelerator
&lt;/h2&gt;

&lt;p&gt;To make this work, the codebase had to be &lt;strong&gt;deliberately un-clever&lt;/strong&gt;. I designed a strict vertical-slice architecture where each package (like &lt;code&gt;pkg/login&lt;/code&gt; or &lt;code&gt;pkg/token&lt;/code&gt;) owns its exact slice of functionality with a predictable structure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;model.go&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;handler.go&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;service.go&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Database CRUD&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This strictness had a massive secondary benefit: it created the &lt;strong&gt;perfect environment for AI&lt;/strong&gt;. Because I spent the time establishing this blueprint, I reached a tipping point where I could hand off the boilerplate. AI agents seamlessly followed the patterns to generate the CRUD operations and rapidly write over &lt;strong&gt;700 tests&lt;/strong&gt; (hitting &lt;strong&gt;80% coverage&lt;/strong&gt;) precisely because the architectural constraints were so rigid.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Scale Ceiling (And Why It Doesn't Matter)
&lt;/h2&gt;

&lt;p&gt;The immediate pushback to this architecture is always: &lt;em&gt;"SQLite doesn't scale."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I am intentionally honest about the scale ceiling: SQLite serializes writes. Auténtico is &lt;strong&gt;not&lt;/strong&gt; designed for active-active multi-region deployments or massive enterprise horizontal scaling.&lt;/p&gt;

&lt;p&gt;However, let's look at the math:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concurrency&lt;/th&gt;
&lt;th&gt;Error rate&lt;/th&gt;
&lt;th&gt;Login p95&lt;/th&gt;
&lt;th&gt;Token p95&lt;/th&gt;
&lt;th&gt;Assessment&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;20 VUs&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;86ms&lt;/td&gt;
&lt;td&gt;54ms&lt;/td&gt;
&lt;td&gt;Comfortable — imperceptible to users&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100 VUs&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;611ms&lt;/td&gt;
&lt;td&gt;647ms&lt;/td&gt;
&lt;td&gt;Supported — fully functional&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;500 VUs&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;3.36s&lt;/td&gt;
&lt;td&gt;3.89s&lt;/td&gt;
&lt;td&gt;Degraded — users feel the wait&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Performance tests with k6 show the system degrades gracefully via SQLite's busy timeout—queueing requests and adding latency rather than throwing errors.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For most teams running internal tools, small-to-mid-sized apps, or self-hosted environments, &lt;strong&gt;trading infinite horizontal scaling for zero operational overhead is absolutely the right choice&lt;/strong&gt;.&lt;/p&gt;




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

&lt;p&gt;Operational simplicity does not mean protocol simplicity.&lt;/p&gt;

&lt;p&gt;Auténtico strictly enforces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;OIDC Discovery&lt;/strong&gt; — publishes &lt;code&gt;/.well-known/openid-configuration&lt;/code&gt; so relying parties auto-configure without hardcoding endpoints&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JWK Set&lt;/strong&gt; — exposes public signing keys at &lt;code&gt;/.well-known/jwks.json&lt;/code&gt; for independent token verification&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RS256 JWT Signing&lt;/strong&gt; — asymmetric signing; the private key never leaves the IdP&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auth2/OIDC protocol&lt;/strong&gt;: ImplementsOIDC protocol&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Admin UI&lt;/strong&gt;: For admins to manage clients, users and session&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Account UI&lt;/strong&gt;: For users to manage they profile&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Swagger OpenAPI docs&lt;/strong&gt;: Publishes api specs docs &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are a small team, an indie developer, or just someone who wants to deploy an Identity Provider without taking on a second job as a sysadmin, sometimes &lt;strong&gt;the best architecture is the one you barely have to think about&lt;/strong&gt;.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://autentico.top/" rel="noopener noreferrer"&gt;Auténtico&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/eugenioenko/autentico" rel="noopener noreferrer"&gt;Github&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>identity</category>
      <category>idp</category>
      <category>go</category>
      <category>sqlite</category>
    </item>
    <item>
      <title>The Lightweight JavaScript Framework Renaissance of 2026</title>
      <dc:creator>Eugene Yakhnenko</dc:creator>
      <pubDate>Tue, 24 Mar 2026 01:43:52 +0000</pubDate>
      <link>https://dev.to/eugenioenko/the-lightweight-javascript-framework-renaissance-of-2026-4ee0</link>
      <guid>https://dev.to/eugenioenko/the-lightweight-javascript-framework-renaissance-of-2026-4ee0</guid>
      <description>&lt;h1&gt;
  
  
  Best JavaScript Frameworks in 2026: For AI and Humans
&lt;/h1&gt;

&lt;p&gt;The JavaScript framework landscape in 2026 looks different from what it did three years ago. Not because React disappeared or Vue lost relevance, but because something shifted in how code gets written. AI coding assistants now author a significant portion of frontend code. That changes the evaluation criteria in ways the existing framework rankings haven't caught up with yet.&lt;/p&gt;

&lt;p&gt;This article covers both the established giants and the growing category of lightweight libraries that are having a quiet renaissance. The goal is to help you pick the right tool given who, or what, will be writing most of your code.&lt;/p&gt;




&lt;h2&gt;
  
  
  The New Evaluation Criteria
&lt;/h2&gt;

&lt;p&gt;The classic framework checklist covered performance, ecosystem, learning curve, and job market. Those still matter. But in 2026, two new questions belong on that list:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How much does this framework cost an AI to get right?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every framework has footguns. The question is whether those footguns require deep framework-specific knowledge to avoid, or whether they're the kind of mistakes any developer (human or AI) would catch on a first read. Frameworks with fewer implicit rules produce more reliable AI-generated code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can you run it without a build pipeline?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For quick prototypes, internal tools, and AI-generated demos, the ability to drop a script tag and go is genuinely valuable. Not every project needs a bundler, and forcing one adds friction that compounds when an AI agent is setting up the environment.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Heavy Framework Tax
&lt;/h2&gt;

&lt;p&gt;React, Vue, Angular, and Svelte dominate the ecosystem. They dominate for real reasons: massive communities, mature tooling, rich ecosystems, and years of production hardening. None of what follows is an argument to abandon them.&lt;/p&gt;

&lt;p&gt;But they carry weight.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;React&lt;/strong&gt; requires understanding hooks ordering rules, &lt;code&gt;useEffect&lt;/code&gt; dependency arrays, stale closure behavior, and the distinction between controlled and uncontrolled components. These are not obvious from the surface syntax. AI agents generating React code make predictable, repeatable mistakes in all of these areas. The community has documented them extensively, which means LLMs have seen the patterns, but also means the footguns are well-established and hard to train away.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vue 3&lt;/strong&gt; is more approachable. The Composition API is clean, and &lt;code&gt;&amp;lt;script setup&amp;gt;&lt;/code&gt; reduces boilerplate significantly. The reactivity model is intuitive. But the template compiler is a black box, the distinction between &lt;code&gt;ref&lt;/code&gt; and &lt;code&gt;reactive&lt;/code&gt; trips up new users (human and AI alike), and the ecosystem split between Options API and Composition API adds cognitive overhead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Angular&lt;/strong&gt; is the most structured of the group. Structure helps, but Angular's DI system, decorators, zone.js, and now the signals migration mean there is a lot of framework-specific knowledge required before you can write idiomatic code. It remains the right choice for large enterprise teams where that structure is the point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Svelte&lt;/strong&gt; compiles away at build time, which is elegant. But the compiler is the framework. You cannot use Svelte without a build step, the template syntax is non-standard HTML, and the reactivity model (especially the &lt;code&gt;$:&lt;/code&gt; syntax in Svelte 4 and the runes in Svelte 5) requires knowing Svelte specifically. An AI agent that hasn't seen enough Svelte in training will produce subtly wrong reactive code.&lt;/p&gt;

&lt;p&gt;None of this is fatal. Millions of applications run on these frameworks and will continue to. But there is a real cost, and it is higher when an AI is holding the pen.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Light Library Renaissance
&lt;/h2&gt;

&lt;p&gt;A different category of tools has been growing steadily: small, focused libraries that add reactivity and component structure on top of the browser's native model rather than replacing it. They tend to share a few traits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No build step required for core functionality&lt;/li&gt;
&lt;li&gt;Templates that stay close to HTML, or use standard JS tagged literals&lt;/li&gt;
&lt;li&gt;Signal-based or proxy-based reactivity with simple rules&lt;/li&gt;
&lt;li&gt;Minimal framework-specific concepts to learn&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In 2026, this category is no longer a niche. It is a legitimate choice for a wide range of projects, and in many cases the better choice for AI-generated code.&lt;/p&gt;




&lt;h2&gt;
  
  
  Arrow.js
&lt;/h2&gt;

&lt;p&gt;Arrow.js (&lt;code&gt;@arrow-js/core&lt;/code&gt;) is one of the most technically interesting entries in this space. It was built by Standard Agents and has an architecture that reads like a deliberate response to framework complexity.&lt;/p&gt;

&lt;p&gt;The core model is simple: reactive state is a plain object wrapped in &lt;code&gt;reactive()&lt;/code&gt;, and templates are JavaScript tagged literals using the &lt;code&gt;html&lt;/code&gt; tag.&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;reactive&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@arrow-js/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;reactive&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="s2"&gt;`
  &amp;lt;div&amp;gt;
    &amp;lt;p&amp;gt;Count: &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;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/p&amp;gt;
    &amp;lt;button @click="&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;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&amp;gt;+&amp;lt;/button&amp;gt;
  &amp;lt;/div&amp;gt;
`&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;app&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things stand out here. Reactive expressions in templates are just arrow functions. Static values render once; functions are tracked and re-run when dependencies change. That distinction is explicit in the syntax, not hidden behind a compiler.&lt;/p&gt;

&lt;p&gt;The reactivity model is proxy-based rather than signal-based. This means you mutate properties directly (&lt;code&gt;state.count++&lt;/code&gt;), and array mutations like &lt;code&gt;.push()&lt;/code&gt; trigger updates without requiring a reassignment. For developers coming from plain JavaScript, this feels natural.&lt;/p&gt;

&lt;p&gt;Components are defined with &lt;code&gt;component()&lt;/code&gt;, a factory function that runs once per slot and returns a template. Local state, side effects, and cleanup all live inside the factory.&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Counter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;component&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;reactive&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;clicks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="s2"&gt;`&amp;lt;button @click="&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;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clicks&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&amp;gt;
    &lt;/span&gt;&lt;span class="p"&gt;${()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clicks&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
  &amp;lt;/button&amp;gt;`&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Arrow's package ecosystem is notably complete. Beyond the core, it ships SSR with &lt;code&gt;@arrow-js/ssr&lt;/code&gt;, client-side hydration with &lt;code&gt;@arrow-js/hydrate&lt;/code&gt;, hydration boundary recovery, and a QuickJS/WASM sandbox (&lt;code&gt;@arrow-js/sandbox&lt;/code&gt;) for safely running user-authored Arrow code in the browser. That last one is unusual and speaks to an AI-native use case: letting agents generate and execute code without granting them access to the host page.&lt;/p&gt;

&lt;p&gt;The tradeoff is that templates live in JavaScript. The &lt;code&gt;html&lt;/code&gt; tagged literal approach means your markup is a JS string, not a file an HTML tool understands natively. That is a different philosophy from the HTML-first camp, and whether it is a feature or a limitation depends on the project.&lt;/p&gt;




&lt;h2&gt;
  
  
  Kasper.js
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://kasperjs.top" rel="noopener noreferrer"&gt;Kasper.js&lt;/a&gt; takes the opposite position on the templates-vs-JavaScript question. Templates are valid HTML. Directives are standard HTML attributes prefixed with &lt;code&gt;@&lt;/code&gt;. Any developer who knows HTML can read a Kasper template without knowing Kasper.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;template&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Count: {{count.value}}&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;on:click=&lt;/span&gt;&lt;span class="s"&gt;"increment()"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;+&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;if=&lt;/span&gt;&lt;span class="s"&gt;"count.value &amp;gt; 10"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;You clicked a lot.&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Component&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="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;kasper-js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Counter&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Component&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;signal&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="nf"&gt;increment&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;count&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="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reactivity model uses signals with explicit &lt;code&gt;.value&lt;/code&gt; reads and writes. This is more verbose than Arrow's proxy approach, but it makes reactive reads visible in both code and templates. An AI agent reading a Kasper template knows exactly which values are reactive and when they update.&lt;/p&gt;

&lt;p&gt;Components are classes. This is a deliberate choice for AI compatibility: classes have well-defined ownership, explicit methods, and a lifecycle that maps directly to familiar OOP patterns. There are no hook rules, no dependency arrays, no rules about where you can call what. &lt;code&gt;onMount&lt;/code&gt;, &lt;code&gt;onChanges&lt;/code&gt;, &lt;code&gt;onRender&lt;/code&gt;, &lt;code&gt;onDestroy&lt;/code&gt;: the lifecycle is what it says it is.&lt;/p&gt;

&lt;p&gt;Cleanup is handled through a single &lt;code&gt;AbortController&lt;/code&gt; that every component owns. &lt;code&gt;this.watch()&lt;/code&gt;, &lt;code&gt;this.effect()&lt;/code&gt;, &lt;code&gt;this.computed()&lt;/code&gt;, and all &lt;code&gt;@on:&lt;/code&gt; event listeners are released automatically when the component is destroyed. No &lt;code&gt;return () =&amp;gt; cleanup()&lt;/code&gt;. No forgetting to unsubscribe.&lt;/p&gt;

&lt;p&gt;The expression evaluator is worth noting: it is a custom recursive-descent parser, not &lt;code&gt;eval&lt;/code&gt; and not &lt;code&gt;new Function&lt;/code&gt;. This means Kasper templates work under strict Content Security Policies, which matters for enterprise and regulated environments. The parser is more capable than it might sound: it covers the full practical range of JavaScript expressions, including arrow functions, optional chaining, nullish coalescing, object and array literals with spread, &lt;code&gt;typeof&lt;/code&gt;, &lt;code&gt;instanceof&lt;/code&gt;, postfix and prefix operators, and a pipeline operator (&lt;code&gt;|&amp;gt;&lt;/code&gt;). The only meaningful gaps compared to full JavaScript are statement-level constructs like &lt;code&gt;async/await&lt;/code&gt;, &lt;code&gt;for&lt;/code&gt; loops, and &lt;code&gt;switch&lt;/code&gt;, none of which belong in a template expression anyway.&lt;/p&gt;

&lt;p&gt;The no-build-step story is genuine. One CDN import (16KB gzipped) and you have signals, a router, slots, and lazy loading. The Vite plugin adds single-file &lt;code&gt;.kasper&lt;/code&gt; components on top, but it is optional.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"module"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;App&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Component&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="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://cdn.jsdelivr.net/npm/kasper-js/dist/kasper.min.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

  &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Counter&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Component&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;signal&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="nx"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;template&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&amp;lt;button @on:click="count.value++"&amp;gt;{{count.value}}&amp;lt;/button&amp;gt;`&lt;/span&gt;

  &lt;span class="nc"&gt;App&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;root&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;counter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Counter&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="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kasper also ships an &lt;code&gt;llms.txt&lt;/code&gt; at &lt;a href="https://kasperjs.top/llms.txt" rel="noopener noreferrer"&gt;kasperjs.top/llms.txt&lt;/a&gt;, a machine-readable reference file specifically for AI agents. It covers the full API surface in a compact format designed for context windows, which reflects where the framework sees the ecosystem going.&lt;/p&gt;




&lt;h2&gt;
  
  
  Others Worth Knowing
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Alpine.js&lt;/strong&gt; is the simplest entry point in the light library category. Directives live directly on HTML elements as attributes (&lt;code&gt;x-data&lt;/code&gt;, &lt;code&gt;x-show&lt;/code&gt;, &lt;code&gt;x-on:click&lt;/code&gt;). There is no component system, no build step, and very little to learn. It is excellent for adding interactivity to server-rendered pages. It is not the right tool for building SPAs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lit&lt;/strong&gt; comes from Google and is built on Web Components. Templates use tagged literals like Arrow, and reactivity is property-based. Lit components are real custom elements, which means they work in any framework or no framework. The tradeoff is that Web Component conventions (shadow DOM, attribute reflection, property vs attribute distinctions) add complexity that pure library approaches avoid.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solid.js&lt;/strong&gt; is a compiler-based framework like Svelte, but its output is fine-grained reactive updates with no virtual DOM. Performance is exceptional. The JSX surface looks like React, which helps with adoption, but the mental model is fundamentally different: components run once, and reactivity is tracked through signal reads. Solid is worth learning if performance is a primary concern and you are comfortable with a build step.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Petite-Vue&lt;/strong&gt; is a distribution of Vue designed for progressive enhancement. It is small, requires no build step, and works well when you need Vue's template syntax on an existing server-rendered page. It is not a full SPA framework.&lt;/p&gt;




&lt;h2&gt;
  
  
  Comparison at a Glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Framework&lt;/th&gt;
&lt;th&gt;Build Required&lt;/th&gt;
&lt;th&gt;Reactivity&lt;/th&gt;
&lt;th&gt;Template Style&lt;/th&gt;
&lt;th&gt;AI-Friendly&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;React&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Hooks&lt;/td&gt;
&lt;td&gt;JSX&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vue 3&lt;/td&gt;
&lt;td&gt;Optional&lt;/td&gt;
&lt;td&gt;Proxy/Ref&lt;/td&gt;
&lt;td&gt;HTML + compiler&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Angular&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Signals/Zone&lt;/td&gt;
&lt;td&gt;HTML + compiler&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Svelte 5&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Runes&lt;/td&gt;
&lt;td&gt;HTML + compiler&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Arrow.js&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Proxy&lt;/td&gt;
&lt;td&gt;JS tagged literals&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kasper.js&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Signals&lt;/td&gt;
&lt;td&gt;Valid HTML&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Alpine.js&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Proxy&lt;/td&gt;
&lt;td&gt;Inline HTML&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lit&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Properties&lt;/td&gt;
&lt;td&gt;JS tagged literals&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Solid&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Signals&lt;/td&gt;
&lt;td&gt;JSX&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  How to Choose
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Use React, Vue, or Angular&lt;/strong&gt; when you are joining an existing team, building a product that needs a large ecosystem, or hiring for a team that already knows the framework. The community, tooling, and hiring pool are real advantages that lightweight alternatives cannot match yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use Svelte or Solid&lt;/strong&gt; when bundle size and runtime performance are primary constraints and you are comfortable with a compiler in the pipeline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use Arrow.js&lt;/strong&gt; when you want the smallest possible runtime, prefer JavaScript-centric templates, need SSR with hydration out of the box, or are building tooling where the sandbox package is relevant.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use Kasper.js&lt;/strong&gt; when HTML-first templates matter (for readability, CSP compliance, or AI-generated code), when you want class-based components with automatic cleanup, or when a no-build-step option has real value for your workflow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use Alpine.js&lt;/strong&gt; when you have server-rendered HTML and want to add interactivity without touching the build pipeline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use Lit&lt;/strong&gt; when Web Components interoperability is a requirement.&lt;/p&gt;




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

&lt;p&gt;The right framework in 2026 is still context-dependent. The established four are not going anywhere, and for many teams they remain the correct answer.&lt;/p&gt;

&lt;p&gt;But the light library category has matured. Arrow.js and Kasper.js in particular are not toys or experiments: they are complete, well-tested solutions with clear architectural philosophies. They are simpler by design, not by omission. And in an era where AI agents write a growing share of frontend code, simpler-by-design has compounding returns.&lt;/p&gt;

&lt;p&gt;The best framework is the one your team, and your tools, can use correctly. In 2026, that calculation includes AI as a member of the team.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>frontend</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Building a JavaScript Framework (and Failing Twice at Reactivity)</title>
      <dc:creator>Eugene Yakhnenko</dc:creator>
      <pubDate>Mon, 23 Mar 2026 01:46:01 +0000</pubDate>
      <link>https://dev.to/eugenioenko/building-a-javascript-framework-and-failing-twice-at-reactivity-5aod</link>
      <guid>https://dev.to/eugenioenko/building-a-javascript-framework-and-failing-twice-at-reactivity-5aod</guid>
      <description>&lt;p&gt;About five years ago, I didn't set out to build a framework I'd use in production.&lt;/p&gt;

&lt;p&gt;I just wanted to understand them.&lt;/p&gt;

&lt;p&gt;I had already written parsers and interpreters before, so I knew the mechanics: tokenization, ASTs, execution. But frameworks felt different. They weren't just about parsing code; they were about state, updates, and keeping the UI in sync. Reactivity was the part I didn't understand. So I decided to build one from scratch.&lt;/p&gt;

&lt;p&gt;I started with the pieces I knew: a scanner, a parser, a JavaScript interpreter, and an HTML template parser. After a while, I had a working system: a small component model and a template engine that could render real views. It looked like a framework.&lt;/p&gt;

&lt;p&gt;But it was missing the one thing that actually makes a framework feel alive: reactivity.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;template&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;{{count.value}}&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"actions"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;on:click=&lt;/span&gt;&lt;span class="s"&gt;"count -= 1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;-&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;on:click=&lt;/span&gt;&lt;span class="s"&gt;"count += 1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;+&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
  &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Component&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;kasper-js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Counter&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Component&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;signal&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="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;h1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Part That Failed Twice
&lt;/h2&gt;

&lt;p&gt;I tried implementing reactivity early on. It didn't work. I've tried with using Proxy, I tried just using a render() function, it was not clicking. There where other parts of the framework I struggled with as well, but reactivity left an imprint.&lt;/p&gt;

&lt;p&gt;Later, I tried again. This time I got something running; but it was fragile. It only worked for a single component. Child updates broke. State invalidation was inconsistent. It looked like reactivity, but you couldn't trust it.&lt;/p&gt;

&lt;p&gt;So I dropped it again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Coming Back to It
&lt;/h2&gt;

&lt;p&gt;The project sat dormant for a long time, almost abandoned. Other projects took over. Life moved on. After a few years away, I returned to it. This time, I approached it differently. Instead of trying to "add reactivity," I focused on correctness first, testing everything, and simplifying assumptions.&lt;/p&gt;

&lt;p&gt;With the help of AI agents, I rebuilt the system around signals.&lt;/p&gt;

&lt;p&gt;That changed everything.&lt;/p&gt;

&lt;p&gt;Once signals were in place, the architecture became much simpler: no virtual DOM, no diffing, direct updates to real DOM nodes, fine-grained reactivity. But the real breakthrough wasn't just the model. It was the process.&lt;/p&gt;

&lt;h2&gt;
  
  
  600 Tests Later
&lt;/h2&gt;

&lt;p&gt;I started adding tests. Then more tests. Then hundreds more. With AI assistance, I reached 600+ test cases. At that point, something unexpected happened: the AI couldn't generate any new meaningful tests. Everything obvious and most non-obvious cases were already covered.&lt;/p&gt;

&lt;p&gt;The test were meaningful. It felt complete.&lt;/p&gt;

&lt;p&gt;But it wasn't. Of course it wasn't. Just because 600+ tests pass it doesn't mean your system has no bugs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Test Wasn't Tests
&lt;/h2&gt;

&lt;p&gt;The codebase looked solid. The tests passed. But there was still a problem: no one had actually used the framework to build real apps.&lt;/p&gt;

&lt;p&gt;So I tried something different. Instead of writing apps manually, I asked AI agents to build them.&lt;/p&gt;

&lt;p&gt;And they failed immediately.&lt;/p&gt;

&lt;p&gt;Not because the framework was broken; but because the AI didn't know how to use it. This was a surprising moment. The system worked, but it wasn't understandable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Missing Piece: Documentation for AI
&lt;/h2&gt;

&lt;p&gt;That's when I introduced something modern: &lt;code&gt;llms.txt&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A dedicated, structured specification designed for AI agents. Not marketing docs. Not tutorials. Just syntax, rules, constraints, and examples. Think of it as a "principal engineer version" of the API.&lt;/p&gt;

&lt;p&gt;Then I started a loop: give the AI the spec, ask it to build an app, observe where it fails, update the spec, repeat.&lt;/p&gt;

&lt;p&gt;After a few iterations, something remarkable happened. The AI started generating full apps on the first try: todo apps, CRUD interfaces, Kanban boards, tree views, infinite scroll, even Game of Life. All working.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/eugenioenko/we-had-to-write-docs-for-ai-llmstxt-changed-everything-44f5"&gt;Article about my experience with llms.txt&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A Surprising Insight About AI
&lt;/h2&gt;

&lt;p&gt;At one point, the AI became very confident about a "necessary" architectural change. It proposed a redesign that would require around 100 lines of changes. We tried it. It failed repeatedly.&lt;/p&gt;

&lt;p&gt;After stepping back and analyzing the problem, the real fix was 5 lines of code.&lt;/p&gt;

&lt;p&gt;That moment stuck with me. AI can be incredibly helpful but it can also confidently overcomplicate problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Tests Stop Helping
&lt;/h2&gt;

&lt;p&gt;With 600+ tests, the system looked stable. But once the AI started generating real applications, new edge cases appeared: subtle rendering issues, lifecycle timing problems, data edge cases that no unit test would have caught in isolation.&lt;/p&gt;

&lt;p&gt;So I kept going. Built more apps (shopping cart, dashboards, editors, product listing, interractive tables with pagination), fed failures back into the system, and added more tests. Real usage found things that testing alone never would.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Actually Made the Framework Stable
&lt;/h2&gt;

&lt;p&gt;Looking back, it wasn't one thing. It was the combination of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A simple reactive model (signals)&lt;/li&gt;
&lt;li&gt;Relentless testing (600+ cases)&lt;/li&gt;
&lt;li&gt;Real-world usage (apps, not just tests)&lt;/li&gt;
&lt;li&gt;AI as both a developer and a user&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Unexpected Win
&lt;/h2&gt;

&lt;p&gt;One design choice I made years ago turned out to be critical: the template syntax was valid HTML. Originally, this was just for better syntax highlighting. But later, it made the framework significantly more AI-friendly. No custom grammar. No ambiguity. Just HTML with extensions.&lt;/p&gt;

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

&lt;p&gt;If I started today, I would design the reactive model first, write tests earlier (a lot earlier), treat AI as a first-class user from day one, and create a machine-readable spec alongside human docs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where It Ended Up
&lt;/h2&gt;

&lt;p&gt;After years of on-and-off work, multiple failures, and hundreds of tests, the framework is stable. Not because it's perfect, but because it survived repeated redesigns, real usage, and constant pressure from both humans and machines.&lt;/p&gt;

&lt;p&gt;Building the framework wasn't the hardest part. Making it correct, usable, and understandable for both humans and AI was the real challenge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it out!
&lt;/h2&gt;

&lt;p&gt;Learn more about kasper.js at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://kasperjs.top" rel="noopener noreferrer"&gt;kasperjs.top/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/eugenioenko/kasper-js" rel="noopener noreferrer"&gt;github.com/eugenioenko/kasper-js&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>javascript</category>
      <category>frameworks</category>
      <category>reactive</category>
      <category>parser</category>
    </item>
    <item>
      <title>We Had to Write Docs for AI: llms.txt Changed Everything</title>
      <dc:creator>Eugene Yakhnenko</dc:creator>
      <pubDate>Mon, 23 Mar 2026 01:37:14 +0000</pubDate>
      <link>https://dev.to/eugenioenko/we-had-to-write-docs-for-ai-llmstxt-changed-everything-44f5</link>
      <guid>https://dev.to/eugenioenko/we-had-to-write-docs-for-ai-llmstxt-changed-everything-44f5</guid>
      <description>&lt;p&gt;Most developers write documentation for humans.&lt;/p&gt;

&lt;p&gt;While building my JavaScript framework, I ran into a problem I didn't expect: the framework worked but AI couldn't use it. Not "wasn't perfect." Not "made small mistakes." It completely failed to build even basic apps correctly unless it had the source code of the framework available.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Moment Things Broke
&lt;/h2&gt;

&lt;p&gt;After years of work, I finally had a stable system: a custom scanner, parser, interpreter, a template engine with components, a signal-based reactivity system, and around 600 tests covering edge cases. I thought I was done.&lt;/p&gt;

&lt;p&gt;So I tried something simple: "Build a todo app using this framework."&lt;/p&gt;

&lt;p&gt;What I got back looked confident, but was completely wrong. Wrong syntax. Wrong mental model. Invented features that didn't exist.&lt;/p&gt;

&lt;p&gt;This wasn't a bug in the framework. It was a documentation failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  README Is Not Enough Anymore
&lt;/h2&gt;

&lt;p&gt;Traditional documentation is designed for humans: narrative explanations, gradual onboarding, examples mixed with storytelling.&lt;/p&gt;

&lt;p&gt;AI doesn't work like that. It doesn't "read" docs. It pattern-matches and guesses. So when the documentation is incomplete, ambiguous, or too prose-heavy, AI fills in the gaps. Confidently. Incorrectly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: llms.txt
&lt;/h2&gt;

&lt;p&gt;The solution was simple in hindsight: treat AI like a strict compiler, not a reader.&lt;/p&gt;

&lt;p&gt;I created a new file: &lt;code&gt;llms.txt&lt;/code&gt;. Not marketing docs. Not tutorials. Just raw, explicit specification.&lt;/p&gt;

&lt;p&gt;The rules were strict:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No prose.&lt;/strong&gt; No storytelling, no explanations. Only syntax, rules, and constraints.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No ambiguity.&lt;/strong&gt; There's a big difference between:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You can use &lt;code&gt;@if&lt;/code&gt; for conditional rendering.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;@if="condition"
&lt;span class="p"&gt;-&lt;/span&gt; condition must be a valid JS expression
&lt;span class="p"&gt;-&lt;/span&gt; evaluates to truthy/falsy
&lt;span class="p"&gt;-&lt;/span&gt; false removes node from DOM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Complete surface area.&lt;/strong&gt; All directives, template expressions, components, lifecycle hooks, signal behavior, everything explicitly defined. Nothing implied.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Minimal but real examples:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;ul&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;li&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;each=&lt;/span&gt;&lt;span class="s"&gt;"item in items"&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;key=&lt;/span&gt;&lt;span class="s"&gt;"item.id"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;{{ item.name }}&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Reading the Docs + Source Code
&lt;/h2&gt;

&lt;p&gt;Even with &lt;code&gt;llms.txt&lt;/code&gt;, the AI couldn't just guess everything. It needed to read a lot of source code, inspect function signatures, understand how signals propagate, see how component lifecycle worked. Only then could it map the spec to the actual implementation and generate working apps.&lt;/p&gt;

&lt;p&gt;Building apps wasn't magic. It was AI + spec + code comprehension.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Iteration Loop
&lt;/h2&gt;

&lt;p&gt;I didn't just hand over the file and hope. The loop looked like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Give AI the current &lt;code&gt;llms.txt&lt;/code&gt; and source access&lt;/li&gt;
&lt;li&gt;Ask it to build a real app (todo, kanban, etc.)&lt;/li&gt;
&lt;li&gt;Observe failures&lt;/li&gt;
&lt;li&gt;Fix the spec&lt;/li&gt;
&lt;li&gt;Repeat&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A few things became clear along the way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Missing features aren't always obvious.&lt;/strong&gt; At one point, AI kept trying to use &lt;code&gt;@keydown.enter&lt;/code&gt;. I had never documented it but the framework already supported it. The fix was to update the spec, not the code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ambiguity is worse than missing features.&lt;/strong&gt; Undocumented features lead to confident guesses. Vaguely documented features lead to confident &lt;em&gt;wrong&lt;/em&gt; guesses. Explicit rules always win.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI exposes your own blind spots.&lt;/strong&gt; It suggested massive architectural rewrites; redesign scope tracking, refactor core systems. All seemed convincing. The result: 100 lines of changes, none of which worked. The real fix? Five lines of code. AI can be very persuasive about the wrong solution.&lt;/p&gt;

&lt;h2&gt;
  
  
  When It Finally Clicked
&lt;/h2&gt;

&lt;p&gt;After a few iterations of refining &lt;code&gt;llms.txt&lt;/code&gt; and reading source code, AI could reliably generate todo apps, Kanban boards, tree views, infinite scroll, and Game of Life (first try), fully working, following spec.&lt;/p&gt;

&lt;p&gt;Real apps also exposed edge cases that 600 unit tests never would: shopping carts, form wizards, markdown editors, live dashboards. The tests covered everything within a known model. Real usage kept expanding the model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two Types of Documentation
&lt;/h2&gt;

&lt;p&gt;There are now two distinct audiences for docs:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Human docs&lt;/strong&gt;: explain concepts, tell the story, teach mental models.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI docs (&lt;code&gt;llms.txt&lt;/code&gt;)&lt;/strong&gt;: define rules, eliminate ambiguity, maximize correctness.&lt;/p&gt;

&lt;p&gt;Both are necessary. They serve completely different purposes and shouldn't be conflated.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Unexpected Payoff
&lt;/h2&gt;

&lt;p&gt;One design decision made early on turned out to help here too: the template syntax was valid HTML. This meant free syntax highlighting, editor support, and it turns out, AI-friendly defaults. The more your syntax looks like existing patterns, the less AI has to guess.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thought
&lt;/h2&gt;

&lt;p&gt;The hardest part wasn't building the framework. It wasn't reactivity or performance.&lt;/p&gt;

&lt;p&gt;It was making the system understandable to something that doesn't actually understand.&lt;/p&gt;

&lt;p&gt;We're entering a world where humans write ideas and AI writes implementations. In that world, specification becomes the product. Not just a supplement to the code, the thing that makes the code usable at all.&lt;/p&gt;

&lt;p&gt;Learn more about kasper.js at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://kasperjs.top" rel="noopener noreferrer"&gt;kasperjs.top&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kasperjs.top/guides/agents/" rel="noopener noreferrer"&gt;Using kasperjs with AI Agents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/eugenioenko/kasper-js" rel="noopener noreferrer"&gt;github.com/eugenioenko/kasper-js&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://llmstxt.org/" rel="noopener noreferrer"&gt;llmstxt.org&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>documentation</category>
      <category>javascript</category>
      <category>llms</category>
    </item>
    <item>
      <title>Adding Attribute-Based Access Control to a Real-Time Collaborative App with OpenTDF</title>
      <dc:creator>Eugene Yakhnenko</dc:creator>
      <pubDate>Fri, 20 Mar 2026 07:52:05 +0000</pubDate>
      <link>https://dev.to/eugenioenko/adding-attribute-based-access-control-to-a-real-time-collaborative-app-with-opentdf-76e</link>
      <guid>https://dev.to/eugenioenko/adding-attribute-based-access-control-to-a-real-time-collaborative-app-with-opentdf-76e</guid>
      <description>&lt;p&gt;I built &lt;a href="https://github.com/eugenioenko/skedoodle" rel="noopener noreferrer"&gt;Skedoodle&lt;/a&gt;, an open-source real-time collaborative sketching app. Think a lightweight Figma for doodling: multiple users connect over WebSocket, draw on a shared infinite canvas, and see each other's cursors move in real time. It's built with React, TypeScript, Two.js for vector graphics, and Zustand for state management, with an Express backend handling persistence and real-time sync.&lt;/p&gt;

&lt;p&gt;Building the interactive parts was the fun challenge. Throttled rendering at 60fps, path simplification algorithms to keep stroke data lean, touch support, pan and zoom on an infinite canvas, undo/redo that works across multiple collaborators. Skedoodle is a proper interactive app, not a toy demo.&lt;/p&gt;

&lt;p&gt;But it had a glaring gap: &lt;strong&gt;no authorization&lt;/strong&gt;. Authentication? Sure, users logged in via OIDC. But once you were in, you could access any sketch if you knew the ID. Think YouTube: every video is technically accessible if you have the link, even "unlisted" ones. Skedoodle had the same problem. There was no way to control who could see or edit what.&lt;/p&gt;

&lt;p&gt;I needed to fix this. And rather than hand-roll role checks and a collaborators table, I wanted to use a proper policy engine — one that could handle the simple case today and scale to more complex scenarios without rewriting everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  How This Project Started
&lt;/h2&gt;

&lt;p&gt;This whole project started because I was working with an AI agent to generate an &lt;a href="https://opentdf.io/llms.txt" rel="noopener noreferrer"&gt;&lt;code&gt;llms.txt&lt;/code&gt;&lt;/a&gt; for OpenTDF; a structured documentation file designed to give AI agents enough context to work with a platform. Once we had it, the obvious next step was to test it: take a real project with no authorization at all, point an agent at the &lt;code&gt;llms.txt&lt;/code&gt;, and see if it could build a correct ABAC integration from scratch.&lt;/p&gt;

&lt;p&gt;Skedoodle was the perfect candidate. A real collaborative app, with authentication but zero authorization. The experiment: could an AI agent, armed only with OpenTDF's &lt;code&gt;llms.txt&lt;/code&gt; and a description of the access model I wanted, deliver a working integration?&lt;/p&gt;

&lt;h2&gt;
  
  
  Why OpenTDF
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://opentdf.io/" rel="noopener noreferrer"&gt;OpenTDF&lt;/a&gt; is an open-source platform maintained by &lt;a href="https://www.virtru.com/" rel="noopener noreferrer"&gt;Virtru&lt;/a&gt; that provides attribute-based access control (ABAC) alongside end-to-end encryption via the &lt;a href="https://github.com/opentdf/spec" rel="noopener noreferrer"&gt;Trusted Data Format&lt;/a&gt; specification.&lt;/p&gt;

&lt;p&gt;What drew me in was how &lt;strong&gt;lightweight the authorization integration is&lt;/strong&gt;. OpenTDF is known for its encryption capabilities, but the ABAC engine stands entirely on its own. You don't need to encrypt anything to use it. You define policies, and the platform makes access decisions. That's exactly what I needed: a centralized policy engine that could answer "does this user have access to this sketch?" based on attributes rather than hardcoded role checks.&lt;/p&gt;

&lt;p&gt;The ABAC model is straightforward:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You define &lt;strong&gt;namespaces&lt;/strong&gt; and &lt;strong&gt;attributes&lt;/strong&gt; (e.g., &lt;code&gt;https://skedoodle.com/attr/sketch-access&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Each attribute has &lt;strong&gt;values&lt;/strong&gt; and a &lt;strong&gt;rule&lt;/strong&gt; (AnyOf, AllOf, or Hierarchy)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subject mappings&lt;/strong&gt; connect identity provider claims to attribute entitlements&lt;/li&gt;
&lt;li&gt;When someone requests access, the platform evaluates their entitlements against the resource's required attributes and returns &lt;strong&gt;permit or deny&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No SDKs to embed, no agents to deploy. It's a JSON API you call. Your app manages the data, OpenTDF manages the policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Access Model
&lt;/h2&gt;

&lt;p&gt;What I wanted was straightforward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Owner&lt;/strong&gt; creates a sketch and always has full access&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Owner can invite&lt;/strong&gt; other users by username&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Owner can remove&lt;/strong&gt; any collaborator&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Collaborators&lt;/strong&gt; can draw on the sketch and can leave voluntarily&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Collaborators cannot&lt;/strong&gt; remove other collaborators or the owner&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No public access&lt;/strong&gt; — every sketch requires an explicit ABAC grant. Read-only public sharing could be layered on later as a separate attribute.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Simple enough for users to understand, but it needs proper enforcement at every layer: REST API, WebSocket connections, and the real-time command stream.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building It with an AI Agent
&lt;/h2&gt;

&lt;p&gt;I used Claude Code as my coding agent. The agent fetched OpenTDF's &lt;code&gt;llms.txt&lt;/code&gt; at runtime, which gave it the architectural overview, API reference, Connect RPC URL patterns, protobuf enum values, and curl examples it needed to understand the platform.&lt;/p&gt;

&lt;p&gt;The agent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Read the docs and &lt;strong&gt;correctly chose ABAC authorization over full TDF encryption&lt;/strong&gt;, understanding that per-command encryption would be impractical for real-time collaboration&lt;/li&gt;
&lt;li&gt;Designed an attribute scheme (one attribute value per sketch, AnyOf rule) that maps cleanly to the sharing model&lt;/li&gt;
&lt;li&gt;Built the entire integration: REST API, WebSocket authorization, OpenTDF service with subject mapping lifecycle, and client UI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;llms.txt&lt;/code&gt; gave the agent enough context to use the right API patterns without guessing — the correct RPC URL format, the exact enum values for condition operators and boolean types, the entity identifier structure for &lt;code&gt;GetDecisions&lt;/code&gt;. I described the access model I wanted, and it delivered a working integration.&lt;/p&gt;

&lt;p&gt;The ongoing iteration — refining the architecture, debugging access issues, removing redundant layers — was also done collaboratively with the agent, with &lt;code&gt;llms.txt&lt;/code&gt; as the shared reference for how OpenTDF's APIs work. When we hit an issue where ABAC returned PERMIT but the app still denied access, the agent was able to trace the problem because it understood the full authorization flow from the docs.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the Integration Works
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ABAC as the Single Source of Truth
&lt;/h3&gt;

&lt;p&gt;There's no &lt;code&gt;collaborators&lt;/code&gt; table in the database. OpenTDF is the &lt;strong&gt;sole authority&lt;/strong&gt; for access control. The database stores sketches, commands, and users. Who has access to what is entirely managed through OpenTDF subject mappings.&lt;/p&gt;

&lt;p&gt;This is a deliberate design choice. Instead of maintaining a local access control table and keeping it in sync with a policy engine, the application delegates all authorization to OpenTDF. The only local concept of "role" is ownership: the &lt;code&gt;Sketch&lt;/code&gt; table has an &lt;code&gt;ownerId&lt;/code&gt; field. Everything else — who can access which sketch, whether a given user is permitted — comes from ABAC.&lt;/p&gt;

&lt;h3&gt;
  
  
  Policy Structure
&lt;/h3&gt;

&lt;p&gt;On server startup, the service registers Skedoodle's policy structure with OpenTDF:&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="na"&gt;Namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://skedoodle.com&lt;/span&gt;
&lt;span class="na"&gt;Attribute: sketch-access (rule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AnyOf)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each sketch gets its own attribute value. Subject mappings are actively managed as part of the application lifecycle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sketch created&lt;/strong&gt; → register an attribute value, create a subject mapping for the owner&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Collaborator invited&lt;/strong&gt; → create a subject mapping linking the user's username to the sketch's attribute value&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Collaborator removed&lt;/strong&gt; → delete the subject mapping&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Access check&lt;/strong&gt; → call &lt;code&gt;GetDecisions&lt;/code&gt; to verify the user has a valid entitlement&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Sharing Workflow
&lt;/h3&gt;

&lt;p&gt;Three endpoints handle collaboration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST   /api/sketches/:id/collaborators           Owner invites by username
DELETE /api/sketches/:id/collaborators/:username  Owner removes, or user leaves
GET    /api/sketches/:id/collaborators            List who has access
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When an owner invites a collaborator, the app creates a subject mapping in OpenTDF:&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;rpc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;policy.subjectmapping.SubjectMappingService&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;CreateSubjectMapping&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;attributeValueId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;valueId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;read&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="na"&gt;newSubjectConditionSet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;subjectSets&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="na"&gt;conditionGroups&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="na"&gt;booleanOperator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;CONDITION_BOOLEAN_TYPE_ENUM_OR&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;conditions&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="na"&gt;subjectExternalSelectorValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.username&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="na"&gt;operator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SUBJECT_MAPPING_OPERATOR_ENUM_IN&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="na"&gt;subjectExternalValues&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="p"&gt;},&lt;/span&gt;
              &lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells the platform: when a user's Keycloak &lt;code&gt;.username&lt;/code&gt; matches, grant them the sketch's attribute value entitlement.&lt;/p&gt;

&lt;p&gt;Listing collaborators queries &lt;code&gt;ListSubjectMappings&lt;/code&gt; and filters for mappings that match the sketch's attribute value. Removing a collaborator deletes the mapping. There's no local state to keep in sync.&lt;/p&gt;

&lt;h3&gt;
  
  
  Access Checks
&lt;/h3&gt;

&lt;p&gt;Every protected operation — loading a sketch, fetching commands, joining a WebSocket room, saving commands — calls &lt;code&gt;GetDecisions&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;rpc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;authorization.AuthorizationService&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;GetDecisions&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;decisionRequests&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="na"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;read&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
      &lt;span class="na"&gt;entityChains&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="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;entities&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;userName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;username&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;resourceAttributes&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="na"&gt;attributeValueFqns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s2"&gt;`https://skedoodle.com/attr/sketch-access/value/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;sketchId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;allowed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;decisionResponses&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="nx"&gt;decision&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;DECISION_PERMIT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the platform denies access or is unreachable, the request is rejected. This is a deliberate choice — ABAC is the single source of truth, so there's no stale local copy to fall back to. In a production deployment where availability is critical, you'd want to run OpenTDF with redundancy, or introduce a short-lived decision cache as a buffer. For Skedoodle, fail-closed is the right tradeoff: denying access temporarily is better than granting it incorrectly.&lt;/p&gt;

&lt;h3&gt;
  
  
  WebSocket Enforcement
&lt;/h3&gt;

&lt;p&gt;Real-time collaboration adds a wrinkle. You can't call a policy service on every brush stroke. The approach:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Authorize on join&lt;/strong&gt;: call &lt;code&gt;GetDecisions&lt;/code&gt; when a user connects&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enforce at the room level&lt;/strong&gt;: owners and collaborators can draw, the role is set once at join time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kick on revocation&lt;/strong&gt;: when access is removed via the API, immediately disconnect the user
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// When an owner removes a collaborator&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mappingId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;opentdfService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findSubjectMappingId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;targetUsername&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sketchId&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;mappingId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;opentdfService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deleteSubjectMapping&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mappingId&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;room&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rooms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sketchId&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;room&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;room&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;kickClientByUsername&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;targetUsername&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 client handles revocation gracefully with a dialog explaining what happened and options to go back.&lt;/p&gt;

&lt;h3&gt;
  
  
  Listing Sketches from ABAC
&lt;/h3&gt;

&lt;p&gt;To show a user their sketches, the app queries both the database and OpenTDF in parallel:&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;ownedSketches&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;abacSketchIds&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sketch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ownerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="nx"&gt;opentdfService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listSketchIdsForUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;username&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;Owned sketches come from the database. Shared sketches come from OpenTDF by iterating subject mappings and extracting sketch IDs from attribute value FQNs. The two lists are merged, deduped, and returned with roles.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Shows About ABAC
&lt;/h2&gt;

&lt;p&gt;This integration replaced what would typically be a &lt;code&gt;collaborators&lt;/code&gt; join table, a set of role-checking queries, and manual sync logic — with a handful of API calls to a policy engine.&lt;/p&gt;

&lt;p&gt;Where ABAC gets interesting is what happens next. Today Skedoodle's access model is simple: per-sketch, per-user grants. But the same infrastructure supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mapping team membership to sketch access (subject mappings based on group claims instead of individual usernames)&lt;/li&gt;
&lt;li&gt;Classification-based access (new attributes with AllOf or Hierarchy rules)&lt;/li&gt;
&lt;li&gt;Cross-organization sharing (attribute values scoped to external identity providers)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These would be &lt;strong&gt;policy changes&lt;/strong&gt; — new attributes, new subject mappings — not application code changes. The &lt;code&gt;checkAccess()&lt;/code&gt; call stays the same.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Timeline
&lt;/h2&gt;

&lt;p&gt;The entire integration took &lt;strong&gt;one afternoon&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Phase&lt;/th&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Switch identity provider to Keycloak&lt;/td&gt;
&lt;td&gt;15 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Create Keycloak client + test users&lt;/td&gt;
&lt;td&gt;10 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Collaborator API + OpenTDF subject mapping lifecycle&lt;/td&gt;
&lt;td&gt;15 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WebSocket authorization + kick-on-revoke&lt;/td&gt;
&lt;td&gt;15 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Client UI (share dialog, access denied, role badges)&lt;/td&gt;
&lt;td&gt;20 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenTDF ABAC service integration&lt;/td&gt;
&lt;td&gt;15 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Debugging and polish&lt;/td&gt;
&lt;td&gt;20 min&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The OpenTDF integration itself was the smallest piece. Most of the work was building the sharing UX and enforcing access at the WebSocket layer. OpenTDF slotted in cleanly because it's designed to be an authorization service you call, not a framework you restructure your app around.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;ABAC can be your single source of truth for access control.&lt;/strong&gt; Instead of maintaining a collaborators table and keeping it in sync with a policy engine, Skedoodle delegates all authorization to OpenTDF. The application code doesn't contain access control logic beyond "ask OpenTDF and respect the answer."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The integration surface is small.&lt;/strong&gt; Six API operations cover the entire authorization model, callable from any language with plain &lt;code&gt;fetch&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Real-time apps need smart enforcement points.&lt;/strong&gt; You can't call a policy service on every WebSocket message. Authorize on connect, enforce roles at the room level, and handle revocation proactively by kicking disconnected users.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;llms.txt&lt;/code&gt; makes AI-assisted integration practical.&lt;/strong&gt; The agent built a working ABAC integration from documentation alone. Structured, machine-readable docs lower the barrier to adoption — not just for AI agents, but for any developer exploring a new platform.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ABAC scales where RBAC doesn't.&lt;/strong&gt; Roles are fine until you need to express "users in department X with clearance level Y can access resources tagged with classification Z." That sentence maps directly to ABAC attributes. Trying to model it with roles leads to an explosion of role combinations.&lt;/p&gt;

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

&lt;p&gt;The &lt;a href="https://opentdf.io/" rel="noopener noreferrer"&gt;OpenTDF&lt;/a&gt; integration lives in a dedicated fork: &lt;a href="https://github.com/eugenioenko/skedoodle-opentdf" rel="noopener noreferrer"&gt;skedoodle-opentdf&lt;/a&gt;. It includes everything you need to run the full stack locally.&lt;/p&gt;

&lt;p&gt;If you're building an app that needs access control beyond basic ownership — especially if you want centralized policy management or the flexibility to evolve your authorization model over time — ABAC with &lt;a href="https://opentdf.io/" rel="noopener noreferrer"&gt;OpenTDF&lt;/a&gt; is worth a look.&lt;/p&gt;

</description>
      <category>abac</category>
      <category>opentdf</category>
      <category>authorization</category>
    </item>
    <item>
      <title>Kneel Before Zod!</title>
      <dc:creator>Eugene Yakhnenko</dc:creator>
      <pubDate>Fri, 16 Jan 2026 02:56:00 +0000</pubDate>
      <link>https://dev.to/eugenioenko/kneel-before-zod-5edm</link>
      <guid>https://dev.to/eugenioenko/kneel-before-zod-5edm</guid>
      <description>&lt;p&gt;TypeScript has changed the game for JavaScript developers by adding static type checking, but it doesn’t automatically handle data validation. Especially when dealing with external sources like APIs or user inputs.&lt;br&gt;
Lets break down the challenges of data validation in TypeScript, explores possible solutions, and takes a closer look at Zod, a powerful validation library.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why Data Validation Matters in TypeScript
&lt;/h2&gt;

&lt;p&gt;Data validation is all about making sure the data you receive is in the right format and contains the right information. This is especially important when handling external data, like API responses, user input or data from local storage. When you define types in TypeScript, they help during development, but they don’t actually enforce anything at runtime. So even if you expect an API to return a certain structure, TypeScript won’t stop it from giving you something completely different.&lt;br&gt;
You've probably experienced this issue tons of times with errors like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;VM228:1 Uncaught TypeError: Cannot read properties of undefined (reading 'something')&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Compile-Time vs. Runtime-Time Gap
&lt;/h2&gt;

&lt;p&gt;One of the biggest challenges in TypeScript data validation is the difference between what TypeScript checks at compile time and what actually happens at runtime. For example, when you fetch data from an API, TypeScript assumes it matches your type definitions, but in reality, there’s no guarantee.&lt;br&gt;
Same issue when reading from localStorage. Even when &lt;code&gt;JSON.parse()&lt;/code&gt; succeeds, there's no guarantee that the data has the shape you're expecting.&lt;br&gt;
This gap means that without extra validation, your app could end up working with incorrect or unexpected data.&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="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fetchUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/users/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// But nothing ensures data actually matches User interface&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;retrieveUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;User&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="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&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;user&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// But nothing ensures data actually matches User interface&lt;/span&gt;
  &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;API interfaces are contracts, and usually this is not an issue, specially if you are also the maintainer of the API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solutions for TypeScript Data Validation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Type Guards and Assertion Functions
&lt;/h3&gt;

&lt;p&gt;TypeScript's built-in type guards provide a simple validation mechanism:&lt;br&gt;
&lt;a href="https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards" rel="noopener noreferrer"&gt;Type Guards Docs&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;data&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;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;username&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Usage&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;processUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&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="nf"&gt;isUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// TypeScript knows data is User here&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;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;Invalid user data&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach works but becomes unwieldy for complex objects, requiring manual implementation of validation logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Zod as a Solution for TypeScript Validation
&lt;/h2&gt;

&lt;p&gt;Zod is a TypeScript-first schema validation library with static type inference. It allows defining schemas that validate data at runtime while automatically inferring TypeScript types.&lt;br&gt;
&lt;a href="https://zod.dev/" rel="noopener noreferrer"&gt;Zod Docs&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;zod&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;UserSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Extract the inferred type&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;infer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;UserSchema&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;// { id: string; email: string }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The retrieve from local storage function would look like:&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;retrieveUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;User&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="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&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;validatedUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;UserSchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&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;validatedUser&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// User matches the type&lt;/span&gt;
  &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Pros of Zod
&lt;/h2&gt;

&lt;h3&gt;
  
  
  TypeScript-First Design
&lt;/h3&gt;

&lt;p&gt;Zod was built specifically for TypeScript, resulting in excellent type inference and integration with TypeScript's type system. This enables catching type errors during development rather than at runtime.&lt;/p&gt;

&lt;h3&gt;
  
  
  Schema-to-Type Inference
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;z.infer&amp;lt;typeof schema&amp;gt;&lt;/code&gt; pattern allows extracting TypeScript types directly from validation schemas, ensuring perfect alignment between validation and types.&lt;/p&gt;

&lt;h3&gt;
  
  
  Comprehensive Schema Options
&lt;/h3&gt;

&lt;p&gt;Zod supports a wide range of validation options, from simple primitives to complex structures including objects, arrays, tuples, unions, and even functions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handy Zod utility: validateSchemaOrThrow
&lt;/h2&gt;

&lt;p&gt;Here is a handy utility for validating schemas. It will attempt to validate the schema.&lt;br&gt;
When it succeeds it returns the validated data. It will re-throw the combined zod errors when data is invalid.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ZodRawShape&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;zod&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;validateSchemaOrThrow&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;ZodRawShape&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;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ZodObject&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&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;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;ReturnType&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ZodObject&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;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;parse&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;safeParse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;success&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;error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;issues&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;issue&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;issue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="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="nx"&gt;error&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;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is how it would end up being used in a framework route for example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;credentials&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;validateSchemaOrThrow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;LoginSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;authUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;loginUserOrThrow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;credentials&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;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;authUser&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&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;error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Unexpected login error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;409&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;h2&gt;
  
  
  More info at
&lt;/h2&gt;

&lt;p&gt;&lt;a href="[https://zod.dev/]"&gt;https://zod.dev/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>zod</category>
      <category>react</category>
      <category>typescript</category>
      <category>validation</category>
    </item>
    <item>
      <title>Handling Tech Debt while Shipping Features</title>
      <dc:creator>Eugene Yakhnenko</dc:creator>
      <pubDate>Fri, 16 Jan 2026 02:54:13 +0000</pubDate>
      <link>https://dev.to/eugenioenko/handling-tech-debt-while-delivering-features-1g6k</link>
      <guid>https://dev.to/eugenioenko/handling-tech-debt-while-delivering-features-1g6k</guid>
      <description>&lt;p&gt;Picture this: You're halfway through building that exciting new feature everyone's been asking for. You're in the zone. The code is flowing. And then... you discover a bug. Not in your new code—in the old system your feature depends on. What do you do? Fix it now? File a ticket and move on? Pretend you didn't see it? Is it actually a bug or is it a bug in your understanding of the requirements?&lt;/p&gt;

&lt;p&gt;If you've been there (and honestly, who hasn't?), you know this moment of choice happens constantly during development. The reality of building software isn't a clean, linear path from requirements to deployment. It's more like exploring a house where opening one door reveals three more doors you didn't know existed, and sometimes those doors are stuck.&lt;/p&gt;

&lt;p&gt;Let's talk about how to handle this reality without burning out, missing deadlines, or letting your codebase turn into a maintenance nightmare.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters More Than You Think
&lt;/h2&gt;

&lt;p&gt;Here's a sobering fact: when you switch from working on your feature to investigating that bug, it takes your brain about a good amount of time to fully get back into the zone afterward. Not the five minutes you hoped. For a team getting interrupted multiple times a day, that's anywhere from 10-20 hours of lost productivity every week.&lt;/p&gt;

&lt;p&gt;And it's not just about time. Studies show that interrupted tasks take twice as long to complete and contain twice as many errors. It's a vicious cycle: poor code quality from interrupted work creates new bugs, which create more interruptions, which create more poor code.&lt;/p&gt;

&lt;p&gt;But here's some good news: teams that handle these interruptions well don't eliminate them (that's impossible). They build systems to manage them efficiently.&lt;/p&gt;

&lt;h2&gt;
  
  
  First Things First: Not Everything is Urgent
&lt;/h2&gt;

&lt;p&gt;The fastest way to chaos is treating every discovered issue like a five-alarm fire. Most things aren't. You need a simple way to decide what actually needs your attention right now.&lt;/p&gt;

&lt;p&gt;Here's a framework that works:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;P0/Critical&lt;/strong&gt;: System crashes, data loss, security breaches. Drop everything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;P1/High&lt;/strong&gt;: Significant features broken but workarounds exist. Handle this sprint or immediately after.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;P2/Medium&lt;/strong&gt;: Degraded experience but not blocking. Can wait until next sprint if needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;P3/Low&lt;/strong&gt;: Cosmetic issues, minor UX friction. Backlog material.&lt;/p&gt;

&lt;p&gt;The key word here is "actually." Is this &lt;em&gt;actually&lt;/em&gt; critical, or does it just feel urgent because you discovered it today?&lt;/p&gt;

&lt;p&gt;A helpful trick: use RICE scoring for the gray areas. Score each issue on Reach (how many users), Impact (how badly affected), Confidence (how sure you are), and Effort (how hard to fix). Then calculate: &lt;code&gt;(Reach × Impact × Confidence) / Effort&lt;/code&gt;. Higher scores win. This removes emotion from the decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Plan for the Unexpected (Because It Will Happen)
&lt;/h2&gt;

&lt;p&gt;Here's where some teams go wrong: they plan sprints as if nothing unexpected will happen. Every hour is allocated to planned work. When interruptions inevitably arrive, the sprint explodes.&lt;/p&gt;

&lt;p&gt;Successful teams build buffer into every sprint:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;10-15% Corporate overhead&lt;/strong&gt;: Meetings, emails, ceremonies&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;60-75% Planned work&lt;/strong&gt;: Your actual features&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;10-15% Unplanned work&lt;/strong&gt;: The buffer for surprises&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;"But that means we'll deliver less!"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I hear you saying. Actually, no. You'll deliver &lt;em&gt;more consistently&lt;/em&gt; because you're planning realistically. Some sprints, you'll have fewer interruptions and pull ahead. Others, you'll use the full buffer. Over time, it averages out—but without the constant feeling of failure.&lt;/p&gt;

&lt;p&gt;How much buffer do you need? It depends on the team, product, and environment: Track your actual interrupt load for a few sprints and adjust accordingly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Superman Strategy: Protecting Focus Time
&lt;/h2&gt;

&lt;p&gt;For teams dealing with production systems or customer support, here's a game-changer: the &lt;strong&gt;Superman rotation&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Instead of spreading interrupts across everyone (death by a thousand distractions), one person handles all interrupts for a set period—a week, a sprint, whatever makes sense. Everyone else gets uninterrupted focus time.&lt;/p&gt;

&lt;p&gt;Yes, one person's productivity takes a hit. But the rest of the team's productivity &lt;em&gt;increases&lt;/em&gt;, and the net result is usually positive. Plus, the Superman builds deep knowledge of system issues and common problems.&lt;/p&gt;

&lt;p&gt;Keys to making this work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rotate fairly&lt;/strong&gt;: Nobody should be permanently on interrupt duty&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Provide backup&lt;/strong&gt;: Have a secondary person for escalation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Be realistic&lt;/strong&gt;: Junior developers might need help; that's okay&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Give them side work&lt;/strong&gt;: They can tackle documentation, tools, or admin tasks between interrupts&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Turn Fires Into Fireproofing
&lt;/h2&gt;

&lt;p&gt;The difference between reactive and proactive teams isn't that proactive teams have fewer problems. It's that they prevent the same problem from happening twice.&lt;/p&gt;

&lt;p&gt;After any major issue, follow this pipeline:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Fix the immediate problem&lt;/strong&gt; (the symptom)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conduct a quick Root Cause Analysis&lt;/strong&gt; within 24-48 hours: Why did this happen? Was it missing tests? Unclear requirements? Architecture gap?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create a prevention artifact&lt;/strong&gt;: A runbook, automated test, monitoring rule, or architectural change&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Track and prioritize improvements&lt;/strong&gt;: Work the highest-impact, lowest-effort ones into your tech debt time&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Example: Payment processing bug blocks the team. Don't just fix it, ask why your tests didn't catch it. Should there be integration tests? Add them. Document the scenario. Set up monitoring. Now it won't happen again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Protect Your Brain: Reduce Context Switching
&lt;/h2&gt;

&lt;p&gt;Even well-managed interrupts cause context switching. Here's how to minimize the damage:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reserve focus time blocks&lt;/strong&gt;: Many teams reserve specific hours as interrupt-free. No meetings, no Slack questions (unless production is literally on fire). Make it a team norm.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Set response time expectations&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Interrupt now (call, DM): Production down, security breach&lt;/li&gt;
&lt;li&gt;Same-day response: Code reviews, sprint blockers (within 8 hours)&lt;/li&gt;
&lt;li&gt;Next-day response: General questions, non-urgent bugs (within 24 hours)&lt;/li&gt;
&lt;li&gt;Async only: Status updates, docs (no immediate response needed)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Limit work-in-progress&lt;/strong&gt;: One or two active items per developer, maximum. Finish before starting new work. It feels slower but actually speeds things up.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Missing Requirements Problem
&lt;/h2&gt;

&lt;p&gt;Sometimes the "issue" isn't a bug—it's that you start building and realize the requirements were incomplete. This happens &lt;em&gt;constantly&lt;/em&gt;. If you are not breaking things, you are not solving hard problems, and incomplete requirements are part of that.&lt;/p&gt;

&lt;p&gt;Three-tier response:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Critical path clarification&lt;/strong&gt;: If it blocks current work, pause and clarify immediately with your product owner. This should be a 30-minute conversation, not a three-day delay.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Scope decision&lt;/strong&gt;: Is this part of the current feature? If yes, add it. If no, capture it for later.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Document for next time&lt;/strong&gt;: Update your requirements template so this gap doesn't recur.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Don't fall into the false choice between "delay everything for perfect requirements" and "build something incomplete." Address blocking gaps now; defer the rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measure What Matters
&lt;/h2&gt;

&lt;p&gt;How do you know if your interrupt management is working? Track:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cycle time&lt;/strong&gt;: How long from issue discovery to fix? Faster is better.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deployment frequency&lt;/strong&gt;: Are you shipping consistently or sporadically?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bug escape rate&lt;/strong&gt;: What percentage of bugs reach production?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Developer satisfaction&lt;/strong&gt;: Survey your team on focus time and stress levels.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If any of these are trending wrong, your process needs adjustment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Traps to Avoid
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Priority inflation&lt;/strong&gt;: If 50% of your issues are "P0 critical," your definitions are broken. Typically, 5-10% should be P0.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Treating interrupts as planning failure&lt;/strong&gt;: They're not. They're inevitable in live software. The question is how you handle them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Permanent interrupt duty&lt;/strong&gt;: Rotate fairly or you'll burn people out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Skipping root cause analysis&lt;/strong&gt;: Fixing the 20th payment bug without understanding why they keep happening means you're firefighting forever. Take the time to prevent recurrence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Process creep&lt;/strong&gt;: Don't add so much overhead that the meetings about interrupts are worse than the interrupts themselves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start Simple
&lt;/h2&gt;

&lt;p&gt;You don't need to implement everything at once. Here is a simple four-week plan to get started:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 1&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Define your priority levels and share with the team&lt;/li&gt;
&lt;li&gt;Reserve 10-20% of your sprint for unplanned work&lt;/li&gt;
&lt;li&gt;Start a weekly 15-minute triage meeting&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Week 2-3&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Try Superman rotation if your team handles interrupts&lt;/li&gt;
&lt;li&gt;Protect time blocks as focus time&lt;/li&gt;
&lt;li&gt;Set max 2 active items per developer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Week 4+&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do root cause analysis on major issues&lt;/li&gt;
&lt;li&gt;Track your metrics&lt;/li&gt;
&lt;li&gt;Adjust based on what you learn&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;Building software means dealing with unexpected issues. The question is not if unexpected issues will happen (they will), but rather when.&lt;/p&gt;

&lt;p&gt;Teams that excel at this aren't more talented or better equipped. They just accept reality and build around it: clear prioritization, reserved capacity, focused triage, root cause prevention, and protected focus time.&lt;/p&gt;

&lt;p&gt;Your codebase will never be perfect. There will always be tech debt, bugs, and surprises. But with the right system, you can ship features, maintain quality, and keep your team healthy.&lt;/p&gt;

&lt;p&gt;The best time to start was yesterday. The second-best time is right now.&lt;/p&gt;

</description>
      <category>codequality</category>
      <category>discuss</category>
      <category>softwaredevelopment</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Custom HTTP Interceptors in HLS.js for Video Streaming</title>
      <dc:creator>Eugene Yakhnenko</dc:creator>
      <pubDate>Fri, 16 Jan 2026 02:52:32 +0000</pubDate>
      <link>https://dev.to/eugenioenko/custom-http-interceptors-in-hlsjs-for-video-streaming-236l</link>
      <guid>https://dev.to/eugenioenko/custom-http-interceptors-in-hlsjs-for-video-streaming-236l</guid>
      <description>&lt;h1&gt;
  
  
  Custom HTTP Interceptors in HLS.js for Video Streaming
&lt;/h1&gt;

&lt;p&gt;When building video streaming applications, you might need to intercept HTTP requests for authentication, custom decryption, or analytics during video playback. HLS.js makes this surprisingly straightforward with custom loaders. Let's explore what HLS.js is and how to implement HTTP interceptors for video fragment loading.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is HLS.js?
&lt;/h2&gt;

&lt;p&gt;HLS.js is a JavaScript library that enables HTTP Live Streaming (HLS) playback in browsers that don't natively support it. While Safari handles HLS natively, browsers like Chrome, Firefox, and Edge need HLS.js to parse &lt;code&gt;.m3u8&lt;/code&gt; playlists and stream video fragments seamlessly.&lt;/p&gt;

&lt;p&gt;The library handles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Parsing HLS manifests (&lt;code&gt;.m3u8&lt;/code&gt; files)&lt;/li&gt;
&lt;li&gt;Downloading and buffering video segments&lt;/li&gt;
&lt;li&gt;Adaptive bitrate switching based on network conditions&lt;/li&gt;
&lt;li&gt;Video playback coordination&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;

&lt;p&gt;Install HLS.js via npm or pnpm:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;hls.js
&lt;span class="c"&gt;# or&lt;/span&gt;
pnpm add hls.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Basic Usage
&lt;/h2&gt;

&lt;p&gt;Here's a minimal React implementation:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;HlsPlayer&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;videoRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLVideoElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;playlistUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://example.com/video.m3u8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;video&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;videoRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&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;video&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Hls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isSupported&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;hls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Hls&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nx"&gt;hls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loadSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;playlistUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;hls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attachMedia&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;video&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;video&lt;/span&gt; &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;videoRef&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;controls&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Implementing a Custom HTTP Interceptor
&lt;/h2&gt;

&lt;p&gt;The real power comes when you need to intercept fragment requests. &lt;br&gt;
Create a custom loader by extending &lt;code&gt;Hls.DefaultConfig.loader&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CustomLoader&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Hls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DefaultConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;loader&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;callbacks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="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;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;frag&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Handle video fragments with custom logic&lt;/span&gt;
      &lt;span class="nf"&gt;fetchVideoFragment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;callbacks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&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="c1"&gt;// Use default loader for playlists&lt;/span&gt;
      &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;callbacks&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 &lt;code&gt;context.frag&lt;/code&gt; check distinguishes between playlist requests (handled by the default loader) and video fragment requests (where we apply custom logic).&lt;/p&gt;

&lt;h2&gt;
  
  
  Custom Fragment Fetcher
&lt;/h2&gt;

&lt;p&gt;Here's how to fetch fragments with custom handling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fetchVideoFragment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;callbacks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Add custom headers here if needed&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{{ Bearer token }}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-Custom-Header&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{{Custom value}}&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;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nx"&gt;callbacks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onSuccess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;loaded&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;byteLength&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;retry&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="nx"&gt;context&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;callbacks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;code&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="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;networkError&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;end&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;retry&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="nx"&gt;context&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;This approach lets you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add authentication tokens to fragment requests&lt;/li&gt;
&lt;li&gt;Decrypt encrypted video segments&lt;/li&gt;
&lt;li&gt;Track loading performance metrics&lt;/li&gt;
&lt;li&gt;Implement custom error handling&lt;/li&gt;
&lt;li&gt;Apply transformations to video data before playback&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Wire up the custom loader when initializing HLS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;HlsPlayerHttpInterceptor&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;videoRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLVideoElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;playlistUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://example.com/video.m3u8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;video&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;videoRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&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;video&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Hls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isSupported&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;hls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Hls&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="c1"&gt;// uses the custom loader&lt;/span&gt;
        &lt;span class="na"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CustomLoader&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;

      &lt;span class="nx"&gt;hls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loadSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;playlistUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;hls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attachMedia&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;video&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;video&lt;/span&gt; &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;videoRef&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;controls&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Use Cases
&lt;/h2&gt;

&lt;p&gt;Custom HTTP interceptors are particularly useful for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Protected content&lt;/strong&gt;: Adding authentication headers to video fragment requests&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DRM workflows&lt;/strong&gt;: Decrypting video segments before playback&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Analytics&lt;/strong&gt;: Tracking fragment load times and network performance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Caching strategies&lt;/strong&gt;: Implementing custom caching logic&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A/B testing&lt;/strong&gt;: Routing requests to different CDN endpoints&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>hls</category>
      <category>react</category>
      <category>video</category>
      <category>streaming</category>
    </item>
    <item>
      <title>Shaking Trees in JavaScript Libraries</title>
      <dc:creator>Eugene Yakhnenko</dc:creator>
      <pubDate>Tue, 18 Mar 2025 03:51:58 +0000</pubDate>
      <link>https://dev.to/eugenioenko/shaking-trees-in-javascript-libraries-394e</link>
      <guid>https://dev.to/eugenioenko/shaking-trees-in-javascript-libraries-394e</guid>
      <description>&lt;h1&gt;
  
  
  Tree Shaking in JavaScript Libraries: Default vs. Named Exports
&lt;/h1&gt;

&lt;p&gt;Tree shaking is an optimization technique that eliminates unused code in JavaScript bundles, significantly reducing the size of applications that consume libraries. When developing JavaScript libraries, the export pattern can dramatically impact your consumers' ability to benefit from tree shaking. Let's explore this with some concrete examples.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding Tree Shaking Limitations with Default Exports
&lt;/h2&gt;

&lt;p&gt;Default exports can prevent tree shaking in several patterns used in JavaScript libraries, resulting in bloated bundles for consumers. Let's review some of these patterns.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Object Pattern Problem
&lt;/h3&gt;

&lt;p&gt;One of the most common anti-patterns that prevents tree shaking occurs when developers use default exports to export multiple functionalities as properties of a single object:&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;// utils.js&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;formatDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* implementation */&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;formatMoney&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* implementation */&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;formatPhoneNumber&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* implementation */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;formatDate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;formatCurrency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;formatPhoneNumber&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When consumers import this module, even if they only need one function, they get the entire object:&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;// consumer.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;utils&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./utils&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;utils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;formatDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt; &lt;span class="c1"&gt;// Only using formatDate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this scenario, the bundler cannot determine that &lt;code&gt;formatCurrency&lt;/code&gt; and &lt;code&gt;formatPhoneNumber&lt;/code&gt; are unused because the entire object is being imported as a single unit. This effectively breaks tree shaking, causing all functions to be included in the final bundle. It also makes it impossible for the consumer to only import &lt;code&gt;formatDate&lt;/code&gt; function because the whole object is exported.&lt;/p&gt;

&lt;h3&gt;
  
  
  Namespace Re-exports
&lt;/h3&gt;

&lt;p&gt;Another problematic pattern involves re-exporting namespace imports:&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;// constants.js&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;foo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;foo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bar&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bar&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// namespaced-constants.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;constants&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./constants&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;constants&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// consumer.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;constants&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./namespaced-constants&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;constants&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;foo&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Only using foo&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern prevents tree shaking because the bundler cannot analyze which properties of the re-exported namespace are actually used by consumers. Both &lt;code&gt;foo&lt;/code&gt; and &lt;code&gt;bar&lt;/code&gt; will be included in the final bundle even though only &lt;code&gt;foo&lt;/code&gt; is used.&lt;/p&gt;

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

&lt;p&gt;This issue is particularly prevalent in React component libraries:&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;// components.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Button&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* implementation */&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;Input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* implementation */&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;Select&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* implementation */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;Select&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a consumer only needs one component, they still receive the entire library:&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;// App.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Components&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./components&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;App&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Components&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Button&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;// Only using Button&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Enabling Tree Shaking with Named Exports
&lt;/h2&gt;

&lt;p&gt;Named exports provide a great approach for library authors who want to enable effective tree shaking for their consumers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Individual Named Exports
&lt;/h3&gt;

&lt;p&gt;The most straightforward pattern uses individual named exports:&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;// utils.js&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;formatDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* implementation */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;formatCurrency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* implementation */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;formatPhoneNumber&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* implementation */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Consumers can then import only what they need:&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;// consumer.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;formatDate&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./utils&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nf"&gt;formatDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this approach, bundlers can now analyze the import statements and determine that only &lt;code&gt;formatDate&lt;/code&gt; is used and avoid bundling the unused functions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Effective Re-exports
&lt;/h3&gt;

&lt;p&gt;When aggregating exports from multiple files, using the &lt;code&gt;export *&lt;/code&gt; syntax provides better tree shaking than re-exporting namespace objects:&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;// constants/index.js&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./dateConstants&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./currencyConstants&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./validationConstants&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// consumer.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;isoDateFormatter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./constants&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern allows bundlers to trace imports through multiple modules and include only the necessary code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Component Library Example
&lt;/h3&gt;

&lt;p&gt;For React component libraries, named exports provide superior tree shaking:&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;// components.js&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Button&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* implementation */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* implementation */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Select&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* implementation */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// App.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Button&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./components&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;App&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Button&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or when needing to combine multiple exports into single re-exporting module use &lt;code&gt;export *&lt;/code&gt; format:&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;// index.js&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./buttons&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./inputs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./select&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Modern bundlers like webpack and rollup can easily determine that only the &lt;code&gt;Button&lt;/code&gt; component is being used and exclude &lt;code&gt;Input&lt;/code&gt; and &lt;code&gt;Select&lt;/code&gt; from the final bundle.&lt;/p&gt;

&lt;h3&gt;
  
  
  Practical Impact on Bundle Size
&lt;/h3&gt;

&lt;p&gt;The practical impact of these differences can be substantial in real-world applications. For example, a UI library with 50 components might be 500KB in total size. With proper tree shaking using named exports, an application that only uses 5 of those components might include just 50KB of code. Without tree shaking due to default exports, the entire 500KB library would be included.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Use named exports for all public API components to enable effective tree shaking&lt;/li&gt;
&lt;li&gt;Avoid exporting objects with multiple properties as default exports&lt;/li&gt;
&lt;li&gt;When aggregating exports from multiple files, use &lt;code&gt;export *&lt;/code&gt; or individual named exports rather than namespace objects&lt;/li&gt;
&lt;li&gt;Consider adding ESLint rules to enforce these patterns across your codebase&lt;/li&gt;
&lt;li&gt;When using &lt;code&gt;export default&lt;/code&gt; make sure to export from different modules and not re-export from a single one. If you do need to re-export from single one, only do &lt;code&gt;export { default as Name}&lt;/code&gt; &lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;The choice between default and named exports is not merely stylistic, it has significant implications for the performance of applications that consume your library. Default exports, particularly when used to export multiple functionalities as a single object, can prevent effective tree shaking. In contrast, named exports enable bundlers to precisely identify and include only the code that's actually used.&lt;/p&gt;

&lt;p&gt;TLDR; Stick to named exports, they are clear winners when it comes to tree shaking capabilities.&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
