<?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: Zul Ikram Musaddik Rayat</title>
    <description>The latest articles on DEV Community by Zul Ikram Musaddik Rayat (@devrayat000).</description>
    <link>https://dev.to/devrayat000</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F867084%2F50a0e027-0439-4c8a-8733-1cad7dff769b.jpg</url>
      <title>DEV Community: Zul Ikram Musaddik Rayat</title>
      <link>https://dev.to/devrayat000</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/devrayat000"/>
    <language>en</language>
    <item>
      <title>Building a Transparent Drag-and-Drop Shelf for Windows with Tauri: The Engineering War Stories</title>
      <dc:creator>Zul Ikram Musaddik Rayat</dc:creator>
      <pubDate>Wed, 17 Jun 2026 15:17:43 +0000</pubDate>
      <link>https://dev.to/devrayat000/building-a-transparent-drag-and-drop-shelf-for-windows-with-tauri-the-engineering-war-stories-3c8c</link>
      <guid>https://dev.to/devrayat000/building-a-transparent-drag-and-drop-shelf-for-windows-with-tauri-the-engineering-war-stories-3c8c</guid>
      <description>&lt;p&gt;I recently shipped SnapShelf, a transparent always-on-top "staging shelf" for Windows; you fling files at the right edge of your screen, it slides in, you drop stuff on it, and later you drag that stuff back out into whatever app needs it. Think of it as a temporary holding tray for files, images, text, and URLs while you move things around your desktop.&lt;/p&gt;

&lt;p&gt;It's built with Tauri v2 (Rust backend + React/TS frontend). On paper it's a small app. In practice, almost every "small" feature collided with a deep Windows/WebView2 constraint that wasn't in any quickstart. This post is the engineering autopsy: the problems that actually cost me days, and the non-obvious fixes.&lt;/p&gt;

&lt;p&gt;If you're building anything that touches native drag-and-drop, transparent windows, or MSIX packaging with Tauri, this'll save you some hair.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture, briefly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;One main WebviewWindow. Transparent, undecorated, always-on-top, skipTaskbar.&lt;/li&gt;
&lt;li&gt;A Rust polling thread that reads the cursor position every 50ms via the Win32 API and decides when to open/close the shelf.&lt;/li&gt;
&lt;li&gt;A small set of atomics as the single source of truth for window state, read lock-free by the poll thread and the IPC commands.&lt;/li&gt;
&lt;li&gt;React frontend for the UI; Rust owns all the window-positioning and OS integration.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That sounds clean. Getting there was not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Challenge 1: WebView2 silently refuses drag-and-drop unless the window was composited before the drag started
&lt;/h2&gt;

&lt;p&gt;This was the big one.&lt;/p&gt;

&lt;p&gt;The whole point of the app is dragging files in from Explorer. So naturally my first design was: keep the window hidden (&lt;code&gt;visible: false&lt;/code&gt;), and &lt;code&gt;show()&lt;/code&gt; it the moment a drag approaches the edge. Clean, no startup flash, minimal resource use.&lt;/p&gt;

&lt;p&gt;It did not work. The shelf would slide in, I'd release the file over it, and nothing. No drop event. Ever.&lt;/p&gt;

&lt;p&gt;The cause: WebView2 registers its OLE drop target (&lt;code&gt;RegisterDragDrop&lt;/code&gt; / &lt;code&gt;IDropTarget&lt;/code&gt;) only when the window is first shown and composited by the DWM. If the first &lt;code&gt;show()&lt;/code&gt; happens mid-drag, which is exactly my trigger flow, the drop target isn't registered yet when the file is released. The drag silently does nothing.&lt;/p&gt;

&lt;p&gt;The fix is counterintuitive: start the window "visible" but parked far off-screen.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="c1"&gt;// tauri.conf.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"windows"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"visible"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;// MUST be true so WebView2 composites + registers IDropTarget&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"x"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;-10000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="c1"&gt;// but park it off every monitor so nothing flashes&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"transparent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"decorations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"alwaysOnTop"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"skipTaskbar"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"dragDropEnabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The window is technically "visible" from the OS's perspective, so WebView2 composites it and registers the drop target at startup, but it's at &lt;code&gt;x: -10000&lt;/code&gt;, off every physical monitor, so the user never sees a flash. By the time any drag happens, the drop target has been live for seconds.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Takeaway: "visible" in Tauri config means "composited," not "on a monitor the user can see." For OS drag-drop to work, you need composited-before-drag. Park off-screen instead of hiding.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Challenge 2: dragDropEnabled is ignored if you set it in the Rust builder
&lt;/h2&gt;

&lt;p&gt;Closely related, and a great way to lose an afternoon. Tauri lets you build windows two ways: declaratively in &lt;code&gt;tauri.conf.json&lt;/code&gt;, or imperatively with &lt;code&gt;WebviewWindowBuilder&lt;/code&gt; in Rust.&lt;/p&gt;

&lt;p&gt;There's an open Tauri bug (&lt;a href="https://github.com/tauri-apps/tauri/issues/13761" rel="noopener noreferrer"&gt;#13761&lt;/a&gt;) where &lt;code&gt;dragDropEnabled&lt;/code&gt; only takes effect when the window is declared in &lt;code&gt;tauri.conf.json&lt;/code&gt;. Set it on the Rust builder and it's silently dropped; drag events never fire.&lt;/p&gt;

&lt;p&gt;So: declare the window (and &lt;code&gt;dragDropEnabled: true&lt;/code&gt;) in the config file, not in Rust. This conflicts with a lot of "spawn your window dynamically" advice you'll find, but it's non-negotiable if you need OS drops.&lt;/p&gt;

&lt;h2&gt;
  
  
  Challenge 3: Never call .hide(), it kills the Acrylic backdrop
&lt;/h2&gt;

&lt;p&gt;I wanted that native Windows 11 Acrylic glass look, applied once at startup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[cfg(target_os&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"windows"&lt;/span&gt;&lt;span class="nd"&gt;)]&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;window_vibrancy&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;apply_acrylic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;apply_mica&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;apply_acrylic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;win&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;&lt;span class="nf"&gt;.is_err&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;apply_mica&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;win&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// fallback&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 works great, until you &lt;code&gt;hide()&lt;/code&gt; and &lt;code&gt;show()&lt;/code&gt; the window. On show, the Acrylic effect is gone; you're left with a flat or black backdrop, and re-applying it on every show causes visible flicker and DWM recomposition jank.&lt;/p&gt;

&lt;p&gt;The fix ties back to Challenge 1: I never call &lt;code&gt;hide()&lt;/code&gt; / &lt;code&gt;show()&lt;/code&gt; at all. Showing and hiding the shelf is purely &lt;code&gt;set_position&lt;/code&gt;; slide it on-screen to reveal, park it off-screen to "hide":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;PARK_X&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;i32&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;32000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// guaranteed off all monitors&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;do_show_shelf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nn"&gt;tauri&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;AppHandle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;win&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="nf"&gt;.get_webview_window&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;win&lt;/span&gt;&lt;span class="nf"&gt;.set_position&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;tauri&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;PhysicalPosition&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;shelf_x&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nf"&gt;shelf_y&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;
    &lt;span class="c1"&gt;// emit event so the frontend can play its slide-in transition&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;do_hide_shelf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nn"&gt;tauri&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;AppHandle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;win&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="nf"&gt;.get_webview_window&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="c1"&gt;// emit hide event, then after the CSS transition, park it:&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;win&lt;/span&gt;&lt;span class="nf"&gt;.set_position&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;tauri&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;PhysicalPosition&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PARK_X&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The window is always composited and Acrylic is applied once. The user just sees it slide in and out. Bonus: positioning is cheaper than show/hide and never re-triggers vibrancy.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Takeaway: with transparent/vibrancy windows, treat the window as permanently alive. Move it, don't hide it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Challenge 4: set_focus() breaks the OLE drag you're trying to catch
&lt;/h2&gt;

&lt;p&gt;When the shelf slides in during a drag, my instinct was to &lt;code&gt;set_focus()&lt;/code&gt; so it's ready for interaction. Don't. Calling &lt;code&gt;set_focus()&lt;/code&gt; on the target window mid-drag disrupts Explorer's &lt;code&gt;DoDragDrop&lt;/code&gt; loop; the OLE drag gets interrupted and the drop fails.&lt;/p&gt;

&lt;p&gt;This created an interesting design tension: in Tray mode (where the user opens the shelf manually with no active drag) I do want focus so I can auto-close on blur. But in the drag-in path, focus is poison.&lt;/p&gt;

&lt;p&gt;I solved it with a per-open "close policy"; the show function takes a policy that decides whether to focus:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;CLOSE_POLICY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AtomicU8&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;AtomicU8&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&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;const&lt;/span&gt; &lt;span class="n"&gt;CP_CURSOR_PARK&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// drag-in / hover: close when cursor leaves&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;CP_CURSOR_SLIVER&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// tab mode: collapse to a strip&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;CP_BLUR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;          &lt;span class="c1"&gt;// tray mode: close on focus loss&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;CP_MANUAL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;        &lt;span class="c1"&gt;// pinned: only explicit close&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;do_show_shelf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nn"&gt;tauri&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;AppHandle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// position the window...&lt;/span&gt;
    &lt;span class="n"&gt;CLOSE_POLICY&lt;/span&gt;&lt;span class="nf"&gt;.store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;Ordering&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Relaxed&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// set_focus ONLY when there's no active OLE drag to disrupt:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;CP_BLUR&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;win&lt;/span&gt;&lt;span class="nf"&gt;.set_focus&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 drag-in path passes &lt;code&gt;CP_CURSOR_PARK&lt;/code&gt; (never focuses). Tray open passes &lt;code&gt;CP_BLUR&lt;/code&gt; (focuses, so the blur handler can close it). One enum, no broken drags.&lt;/p&gt;

&lt;h2&gt;
  
  
  Challenge 5: I deleted the "edge detector" window entirely and replaced it with a 50ms poll
&lt;/h2&gt;

&lt;p&gt;The textbook way to detect "user dragged something to the screen edge" is a thin, transparent, always-on-top trigger window glued to the edge that listens for drag-enter. I built that first. It worked, but:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It's a second WebView; more RAM, against my under-30MB idle goal.&lt;/li&gt;
&lt;li&gt;A click-through (&lt;code&gt;set_ignore_cursor_events&lt;/code&gt;) window won't receive OS drag-enter, so it can't be click-through, which means it eats a 2px strip of clicks.&lt;/li&gt;
&lt;li&gt;It's another moving part that can race with the main window.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I scrapped it for a single Rust thread polling the Win32 cursor API directly. It owns both open and close decisions, for every mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[cfg(target_os&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"windows"&lt;/span&gt;&lt;span class="nd"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;start_drag_edge_poll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;tauri&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;AppHandle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;windows_sys&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Win32&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Foundation&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;POINT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;windows_sys&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Win32&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;UI&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;KeyboardAndMouse&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;GetAsyncKeyState&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;windows_sys&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Win32&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;UI&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;WindowsAndMessaging&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;GetCursorPos&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;thread&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;spawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;move&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;hover_dwell&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u8&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="k"&gt;loop&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;thread&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;time&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_millis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;pt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;POINT&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;x&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="n"&gt;y&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;if&lt;/span&gt; &lt;span class="k"&gt;unsafe&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;GetCursorPos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;pt&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="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;lbtn_down&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;unsafe&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;GetAsyncKeyState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0x01&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;u16&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mi"&gt;0x8000&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

            &lt;span class="c1"&gt;// Drag-in: left button held + cursor at right edge -&amp;gt; open. Works in every mode.&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;lbtn_down&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;pt&lt;/span&gt;&lt;span class="py"&gt;.x&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nf"&gt;screen_right&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;in_y_band&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pt&lt;/span&gt;&lt;span class="py"&gt;.y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nf"&gt;do_show_shelf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CP_CURSOR_PARK&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="c1"&gt;// hover-dwell, tab-strip, and auto-close logic follow...&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;50ms is imperceptible to the user but trivial for the CPU (the thread is asleep 99% of the time). No second webview, no click-eating strip, and all the timing logic lives in one place. The polling-thread-as-state-machine pattern turned out far simpler than coordinating two windows via events.&lt;/p&gt;

&lt;h2&gt;
  
  
  Challenge 6: Dragging an item out trips your own auto-close
&lt;/h2&gt;

&lt;p&gt;Here's a fun self-inflicted bug. Auto-close fires when the cursor leaves the shelf rectangle. But dragging a file out of the shelf is literally moving the cursor off the shelf, so the shelf would slam shut mid-drag, cancelling the drag-out.&lt;/p&gt;

&lt;p&gt;Fixed with a flag that the drag-out command raises for its duration, and which the poll loop's close check respects:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;DRAG_OUT_ACTIVE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AtomicBool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;AtomicBool&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nd"&gt;#[tauri::command]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;start_file_drag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;tauri&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;AppHandle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;paths&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;String&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;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;DRAG_OUT_ACTIVE&lt;/span&gt;&lt;span class="nf"&gt;.store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;Ordering&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Relaxed&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;tauri&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;async_runtime&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;spawn_blocking&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;move&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nn"&gt;drag&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;start_drag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="nf"&gt;.map_err&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="n"&gt;DRAG_OUT_ACTIVE&lt;/span&gt;&lt;span class="nf"&gt;.store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;Ordering&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Relaxed&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// self-clearing after the drag completes&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And in the poll loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;CP_CURSOR_PARK&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;CP_CURSOR_SLIVER&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;DRAG_OUT_ACTIVE&lt;/span&gt;&lt;span class="nf"&gt;.load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Ordering&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Relaxed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// don't auto-close mid drag-out&lt;/span&gt;
    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;cursor_outside_shelf_rect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;do_hide_shelf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;app&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;It's self-cleaning: the flag clears when &lt;code&gt;start_drag&lt;/code&gt; returns (after drop), and the next poll tick tidies up if the cursor is now off the shelf.&lt;/p&gt;

&lt;h2&gt;
  
  
  Challenge 7: Shrinking the binary from 17MB to 5.6MB
&lt;/h2&gt;

&lt;p&gt;The first release build was 17MB. For something that markets itself as "lightweight," that stung. Two big levers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Cargo release profile. This alone is enormous:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="c"&gt;# src-tauri/Cargo.toml&lt;/span&gt;
&lt;span class="nn"&gt;[profile.release]&lt;/span&gt;
&lt;span class="py"&gt;opt-level&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"s"&lt;/span&gt;      &lt;span class="c"&gt;# optimize for size&lt;/span&gt;
&lt;span class="py"&gt;lto&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;           &lt;span class="c"&gt;# link-time optimization&lt;/span&gt;
&lt;span class="py"&gt;codegen-units&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;    &lt;span class="c"&gt;# better optimization, slower compile&lt;/span&gt;
&lt;span class="py"&gt;strip&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;         &lt;span class="c"&gt;# strip symbols&lt;/span&gt;
&lt;span class="py"&gt;panic&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"abort"&lt;/span&gt;      &lt;span class="c"&gt;# drop unwinding machinery&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Kill default features on heavy crates. The &lt;code&gt;image&lt;/code&gt; crate pulls in decoders for a dozen formats I'll never see in a clipboard shelf:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="py"&gt;image&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.25"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;default-features&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="py"&gt;features&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"jpeg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"webp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"gif"&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;I also ripped out syntax highlighting entirely. I'd reflexively added Shiki + highlight.js for "code snippet" cards early on; it was about 11MB of the frontend bundle. A clipboard shelf does not need a syntax highlighter. Deleting it took the &lt;code&gt;dist/&lt;/code&gt; from 11MB to 249KB. Sometimes the best optimization is admitting a feature was never needed.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Takeaway: audit your default-features. Most heavy crates pull in things you don't use, and Cargo won't tell you. And question features that drag in megabytes for marginal value.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Challenge 8: A regex that quietly corrupted my MSIX manifest
&lt;/h2&gt;

&lt;p&gt;Last one, and a good "always read the docs for the function signature" lesson.&lt;/p&gt;

&lt;p&gt;For Microsoft Store distribution I pack the MSIX myself with a PowerShell script. It patches the &lt;code&gt;Identity Version&lt;/code&gt; in the manifest from &lt;code&gt;package.json&lt;/code&gt; at pack time. My first version used a regex:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Intended: replace ONLY the Identity version&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$xml&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;Replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$xml&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'Version="\d+\.\d+\.\d+\.\d+"'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Version=""&lt;/span&gt;&lt;span class="nv"&gt;$Version&lt;/span&gt;&lt;span class="s2"&gt;.0"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bundling failed with &lt;code&gt;0x80080215&lt;/code&gt;, and worse, I burned an hour blaming MinVersion and platform-targeting before I looked at the actual emitted manifest. The bug: that trailing &lt;code&gt;1&lt;/code&gt; is not "replace the first match." In .NET's &lt;code&gt;Regex.Replace&lt;/code&gt;, that overload's last parameter is &lt;code&gt;RegexOptions&lt;/code&gt;, and &lt;code&gt;1&lt;/code&gt; is the integer value of &lt;code&gt;RegexOptions.IgnoreCase&lt;/code&gt;. So it replaced every match.&lt;/p&gt;

&lt;p&gt;And &lt;code&gt;MinVersion="10.0.18362.0"&lt;/code&gt; contains the substring &lt;code&gt;Version="..."&lt;/code&gt;. So my "version bumper" was happily rewriting &lt;code&gt;MinVersion&lt;/code&gt; to a garbage value, producing an invalid manifest that &lt;code&gt;makeappx&lt;/code&gt; rejected with an unhelpful error.&lt;/p&gt;

&lt;p&gt;The fix: stop pattern-matching XML, use the XML DOM and target exactly the node I mean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;xml&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$doc&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Get-Content&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$Manifest&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Raw&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Encoding&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;UTF8&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$ns&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;New-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Xml.XmlNamespaceManager&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$doc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NameTable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$ns&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddNamespace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"pkg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://schemas.microsoft.com/appx/manifest/foundation/windows10"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$doc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SelectSingleNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"/pkg:Package/pkg:Identity"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$ns&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$Version&lt;/span&gt;&lt;span class="s2"&gt;.0"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$doc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$out&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Takeaway: two lessons. (1) Don't regex structured formats when a real parser exists. (2) When a build tool throws a cryptic hex error, inspect its actual input before theorizing about the tool.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;ul&gt;
&lt;li&gt;The platform constraints are the product. 80% of the engineering was respecting WebView2/OLE/DWM rules, not writing features.&lt;/li&gt;
&lt;li&gt;"Visible" is not the same as "on screen." Park, don't hide.&lt;/li&gt;
&lt;li&gt;One state-owning thread beats coordinating multiple windows via events. Fewer races, simpler reasoning.&lt;/li&gt;
&lt;li&gt;Tauri's size win is real but not automatic; you still have to tune the release profile and audit features.&lt;/li&gt;
&lt;li&gt;Read the function signature. That &lt;code&gt;1&lt;/code&gt; cost me an afternoon.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The interesting part was always the fight with the platform underneath.&lt;/p&gt;

&lt;p&gt;Happy to go deeper on any of these in the comments. The WebView2 drop-target thing especially; if you're hitting "my drop events never fire," I've probably made your exact mistake.&lt;/p&gt;

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

&lt;p&gt;SnapShelf is free on the Microsoft Store: &lt;a href="https://apps.microsoft.com/store/detail/9N0CW4WQK8B1?cid=DevShareMCLPCS" rel="noopener noreferrer"&gt;Get SnapShelf&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Works on Windows 10 (22H2) and Windows 11.&lt;/p&gt;

</description>
      <category>tauri</category>
      <category>rust</category>
      <category>windows</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How I Built and Launched a 100% Offline AI Productivity Tracker (Tauri 2, Rust, and Llama 3.2)</title>
      <dc:creator>Zul Ikram Musaddik Rayat</dc:creator>
      <pubDate>Thu, 11 Jun 2026 04:49:41 +0000</pubDate>
      <link>https://dev.to/devrayat000/how-i-built-and-launched-a-100-offline-ai-productivity-tracker-tauri-2-rust-and-llama-32-pij</link>
      <guid>https://dev.to/devrayat000/how-i-built-and-launched-a-100-offline-ai-productivity-tracker-tauri-2-rust-and-llama-32-pij</guid>
      <description>&lt;p&gt;After months of late-night coding, debugging, and wrestling with compiler errors, I finally reached a major milestone: &lt;strong&gt;I published my first-ever desktop application to the Microsoft Store.&lt;/strong&gt; 🎉&lt;/p&gt;

&lt;p&gt;The app is called &lt;strong&gt;Focus Stream&lt;/strong&gt;. It tracks how you spend time on your PC and turns it into an AI-generated focus journal. The catch? &lt;strong&gt;It runs fully on-device.&lt;/strong&gt; No accounts, no cloud database, and absolutely no telemetry. Window snapshots, activity timelines, and LLM inference all happen locally.&lt;/p&gt;

&lt;p&gt;In this post, I want to share the architecture behind Focus Stream, how I fit a local Large Language Model (LLM) into a lightweight desktop app, and what it took to package and ship it to the Microsoft Store.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Core Concept: Privacy-First AI
&lt;/h2&gt;

&lt;p&gt;We've all seen the news about cloud-based activity trackers scanning user screens. While productivity tracking is incredibly useful, uploading your screen snapshots, open documents, and active window titles to a third-party server feels like a major privacy risk.&lt;/p&gt;

&lt;p&gt;I wanted to build a tool that gives you the power of AI analysis and natural language query over your day, but with &lt;strong&gt;zero data leaving your machine&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;To make this happen, I designed a local-first architecture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  [ User Desktop ]
         │
         ▼ (Activity logs &amp;amp; snapshots sampled)
  [ Local SQLite Database ]
         │
         ├───────────────────────────────┐
         ▼                               ▼
  [ React 19 Frontend ] ◄──────── [ Local Llama 3.2 1B ]
  (Visual timeline, trends)     (Local inference via mistral.rs)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;I chose a stack that balances web-ecosystem UI flexibility with native-performance system hooks:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Frontend&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;React 19, TypeScript, Vite, Recharts, Base UI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Backend&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Rust, Tauri 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Database&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;SQLite (&lt;code&gt;tauri-plugin-sql&lt;/code&gt; with WAL enabled)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AI Inference&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;mistral.rs&lt;/code&gt; running a quantized Llama 3.2 1B model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Packaging&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;MSIX (x64 and arm64), Microsoft Store&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;


&lt;h2&gt;
  
  
  Deep Dive: Solving the Local LLM Challenge
&lt;/h2&gt;

&lt;p&gt;Running a local LLM inside a desktop application introduces two major hurdles:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;How do you keep the installer small?&lt;/strong&gt; You can't bundle a 1.2 GB GGUF model in the installer without turning away users.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;How do you support multiple architectures and hardware setups (CPU vs. GPU)?&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here is how I solved them.&lt;/p&gt;
&lt;h3&gt;
  
  
  1. The Dynamic DLL &amp;amp; On-Demand Model Download
&lt;/h3&gt;

&lt;p&gt;Instead of compiling the AI inference engine directly into the main binary, I built a modular architecture using compile-time features. &lt;/p&gt;

&lt;p&gt;Focus Stream compiles in production with an &lt;code&gt;ai_dll&lt;/code&gt; feature flag. The core executable is extremely slim. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;First Launch:&lt;/strong&gt; The app prompts the user to download the Llama 3.2 1B model (~1.2 GB quantized GGUF) directly from Hugging Face into their local app data directory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic Linking:&lt;/strong&gt; At runtime, the app checks the system architecture and dynamically loads the compiled C-compatible DLL (&lt;code&gt;focus_stream_cpu.dll&lt;/code&gt; or a future GPU-accelerated equivalent) using the &lt;code&gt;libloading&lt;/code&gt; crate in Rust.
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// A look at how we dynamically load the inference backend at runtime&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;load_backend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Box&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;dyn&lt;/span&gt; &lt;span class="n"&gt;AIBackend&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Box&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;dyn&lt;/span&gt; &lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;unsafe&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;libloading&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Library&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;libloading&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Symbol&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;unsafe&lt;/span&gt; &lt;span class="k"&gt;extern&lt;/span&gt; &lt;span class="s"&gt;"C"&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="k"&gt;dyn&lt;/span&gt; &lt;span class="n"&gt;AIBackend&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; 
            &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="nf"&gt;.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;b"create_backend"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Box&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_raw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
    &lt;span class="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 keeps the base application installer under &lt;strong&gt;70MB&lt;/strong&gt;, downloading the heavy LLM weights only when the user is ready.&lt;/p&gt;
&lt;h3&gt;
  
  
  2. Multi-Architecture CPU Builds (x64 + arm64)
&lt;/h3&gt;

&lt;p&gt;Since Windows runs on both Intel/AMD (x64) and Snapdragon (arm64) architectures, Focus Stream had to support both. &lt;/p&gt;

&lt;p&gt;I wrote a PowerShell script that compiles the CPU DLLs targeting both architectures:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Building the Rust-based CPU backend DLL for ARM64&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;pwsh&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;src-tauri/scripts/build-cpu-backend.ps1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;aarch64-pc-windows-msvc&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;In our CI/CD pipeline, both the &lt;code&gt;x86_64-pc-windows-msvc&lt;/code&gt; and &lt;code&gt;aarch64-pc-windows-msvc&lt;/code&gt; DLLs are bundled as resources, ensuring native execution speeds regardless of the processor.&lt;/p&gt;


&lt;h2&gt;
  
  
  Wrestling with the Microsoft Store &amp;amp; MSIX Packaging
&lt;/h2&gt;

&lt;p&gt;This was my first time packaging a desktop app for the Microsoft Store, and the learning curve was steep. The store requires signing your app package inside a &lt;code&gt;.msix&lt;/code&gt; container.&lt;/p&gt;
&lt;h3&gt;
  
  
  Version Synchronization
&lt;/h3&gt;

&lt;p&gt;To release updates, the Microsoft Store requires a 4-part version structure (e.g., &lt;code&gt;1.0.0.0&lt;/code&gt;) in your XML app manifest, while npm uses SemVer (e.g., &lt;code&gt;1.0.0&lt;/code&gt;). &lt;/p&gt;

&lt;p&gt;To avoid manual mistakes, I wrote a Node script (&lt;code&gt;sync-version.mjs&lt;/code&gt;) that acts as a single source of truth. When I run &lt;code&gt;pnpm version&lt;/code&gt;, the script automatically:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Bumps &lt;code&gt;package.json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Syncs the version to Tauri's config files (&lt;code&gt;tauri.conf.json&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Syncs the cargo version in &lt;code&gt;Cargo.toml&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Propagates the version to the XML elements in &lt;code&gt;Package.Store.appxmanifest&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  CI/CD Automation
&lt;/h3&gt;

&lt;p&gt;Our GitHub Actions pipeline builds both x64 and arm64 MSIX installers, merges them into a single &lt;code&gt;.msixbundle&lt;/code&gt; file, and uses the Windows Partner Center API to submit the build directly to the Microsoft Store when a tag is pushed.&lt;/p&gt;


&lt;h2&gt;
  
  
  Lessons Learned on My First Desktop Launch
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Rust is a perfect match for Tauri 2:&lt;/strong&gt; Writing low-level native logic (like screen capturing, active window polling, and database caching) in Rust is extremely safe and fast, while React lets me build a modern dashboard with ease.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local AI is highly viable:&lt;/strong&gt; You don't need a RTX 4090 to run local inference. Llama 3.2 1B runs surprisingly fast on mid-range laptop CPUs using quantization. It's more than capable of summarizing text journals and doing lightweight RAG.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Packaging is 50% of the battle:&lt;/strong&gt; Building the app is only half the job. Code signing, target architectures, MSIX manifest constraints, and store policy reviews take a significant amount of effort.&lt;/li&gt;
&lt;/ol&gt;


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

&lt;p&gt;Now that the foundation is live on the store, I am working on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Optional GPU Acceleration:&lt;/strong&gt; Packaging CUDA-compatible DLLs that automatically load if a user has a dedicated NVIDIA GPU, falling back to CPU if not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More detailed dashboards:&lt;/strong&gt; Using &lt;code&gt;recharts&lt;/code&gt; to build granular charts tracking productivity trends over months.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Focus Stream is available on the Microsoft Store with a &lt;strong&gt;7-day free trial&lt;/strong&gt; and a one-time purchase of $9.99 (no subscriptions!). If you are a developer, freelancer, or someone looking to analyze your time privately, I'd love for you to check it out.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://apps.microsoft.com/store/detail/9P9VF2T58XGX?cid=DevShareMTwPCS" rel="noopener noreferrer"&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%2Ft2ave7p41515u9jj1am8.png" alt="Get it from Microsoft" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
      &lt;div class="c-embed__body flex items-center justify-between"&gt;
        &lt;a href="https://apps.microsoft.com/store/detail/9P9VF2T58XGX?cid=DevShareMTwPCS" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;apps.microsoft.com&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Have you built desktop apps with Tauri 2? Have you experimented with local LLM integration on client machines? Let's discuss in the comments below!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>productivity</category>
      <category>tauri</category>
    </item>
    <item>
      <title>I Bought My Phone, But Android 16 Owns My Data: The Death of Non-Root Access</title>
      <dc:creator>Zul Ikram Musaddik Rayat</dc:creator>
      <pubDate>Fri, 05 Jun 2026 06:36:33 +0000</pubDate>
      <link>https://dev.to/devrayat000/i-bought-my-phone-but-android-16-owns-my-data-the-death-of-non-root-access-4bf1</link>
      <guid>https://dev.to/devrayat000/i-bought-my-phone-but-android-16-owns-my-data-the-death-of-non-root-access-4bf1</guid>
      <description>&lt;p&gt;I recently tried to do something that should be the digital equivalent of picking up a rock: &lt;strong&gt;copying a single file.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I had a save file (&lt;code&gt;some.save&lt;/code&gt;) for a game sitting in the standard &lt;code&gt;/Android/data/com.app/files/saves/&lt;/code&gt; directory. I bought the phone with my own money. It sits in my pocket. But after spending hours throwing everything from Shizuku to WSL at it, I learned a harsh lesson: &lt;strong&gt;On Android 16, you don't own your data.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Google’s "Scoped Storage" and aggressive SELinux policies have crossed the line from protecting user privacy to completely trapping power users. If your device isn't rooted, your data is locked in a digital fortress.&lt;/p&gt;

&lt;p&gt;Here is the technical breakdown of my descent into Android 16 madness, and how the OS systematically blocked every single advanced workaround I threw at it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempt 1: The Shizuku-Powered File Managers
&lt;/h2&gt;

&lt;p&gt;My first thought was that Scoped Storage was just blocking standard file managers. No problem, right? We have &lt;strong&gt;Shizuku&lt;/strong&gt;, which runs processes via the ADB &lt;code&gt;shell&lt;/code&gt; user (UID 2000).&lt;/p&gt;

&lt;p&gt;I installed ZArchiver and FV File Explorer, authorized them through Shizuku, and navigated to the &lt;code&gt;/Android/data/com.app/&lt;/code&gt; directory.&lt;br&gt;
&lt;strong&gt;Result:&lt;/strong&gt; &lt;code&gt;Permission Denied&lt;/code&gt; / &lt;code&gt;Access Denied&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Android 16 has patched the system-level Java binders. Even with Shizuku privileges, graphical file managers are blinded by the OS security layer.&lt;/p&gt;
&lt;h2&gt;
  
  
  Attempt 2: Dropping into the Terminal
&lt;/h2&gt;

&lt;p&gt;Fine. If the GUI fails, go to the CLI. I fired up Termux, triggered my Shizuku &lt;code&gt;rish&lt;/code&gt; (shell) session, and tried to brute-force a copy.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; /data/data/com.app/files/saves/some.save /sdcard/Download/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; &lt;code&gt;Permission Denied&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Even trying to read the raw text stream using &lt;code&gt;cat /data/data... &amp;gt; /sdcard/...&lt;/code&gt; was slapped down. Android 16’s SELinux policy strictly prohibits the shell user from interacting with physical app sandbox directories.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempt 3: The &lt;code&gt;run-as&lt;/code&gt; Illusion
&lt;/h2&gt;

&lt;p&gt;Android has a built-in debugging command designed specifically for this: &lt;code&gt;run-as&lt;/code&gt;. It lets the shell assume the permissions of the target application.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;run-as com.app &lt;span class="nb"&gt;cat&lt;/span&gt; /data/data/com.app/files/saves/some.save
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; &lt;code&gt;run-as: package not debuggable: com.app&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Because the app's manifest had &lt;code&gt;android:debuggable="false"&lt;/code&gt;, the OS refused to grant the shell access.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempt 4: Modding the APK
&lt;/h2&gt;

&lt;p&gt;If the app isn't debuggable, make it debuggable. I pulled the base APK to my Windows machine, fired up &lt;strong&gt;WSL&lt;/strong&gt;, and used Apktool to decompile it. I injected &lt;code&gt;android:debuggable="true"&lt;/code&gt; into the &lt;code&gt;AndroidManifest.xml&lt;/code&gt;, rebuilt it, and signed it with a debug keystore.&lt;/p&gt;

&lt;p&gt;To avoid signature mismatch errors (which would require uninstalling and losing the save file), I even cloned the app by changing the package name to &lt;code&gt;com.app.debug&lt;/code&gt; so I could install it side-by-side and try to push the save file into the clone's directory.&lt;br&gt;
&lt;strong&gt;Result:&lt;/strong&gt; &lt;code&gt;cat: permission denied&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Even pushing data &lt;em&gt;into&lt;/em&gt; an open, debuggable clone was blocked by the overarching SELinux filesystem locks.&lt;/p&gt;
&lt;h2&gt;
  
  
  Attempt 5: The &lt;code&gt;adb backup&lt;/code&gt; Betrayal
&lt;/h2&gt;

&lt;p&gt;While looking at the decompiled manifest, I noticed a beacon of hope: &lt;code&gt;android:allowBackup="true"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Perfect. I connected my phone to my PC and ran a native ADB backup command via WSL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adb backup &lt;span class="nt"&gt;-noapk&lt;/span&gt; com.app &lt;span class="nt"&gt;-f&lt;/span&gt; game_data.ab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The prompt appeared on my phone, I clicked "Back up my data," and a file was generated. I used &lt;code&gt;dd&lt;/code&gt;, &lt;code&gt;zlib-flate&lt;/code&gt;, and &lt;code&gt;tar&lt;/code&gt; in WSL to strip the 24-byte Android OS header and unpack it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;dd &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;game_data.ab &lt;span class="nv"&gt;bs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="nv"&gt;skip&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;24 | zlib-flate &lt;span class="nt"&gt;-uncompress&lt;/span&gt; | &lt;span class="nb"&gt;tar&lt;/span&gt; &lt;span class="nt"&gt;-xvf&lt;/span&gt; -
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; &lt;code&gt;525 bytes copied... tar: This does not look like a tar archive.&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;A 525-byte file means it’s empty. Android 16 introduces an aggressive background security layer that blocks backup streams for side-loaded apps or apps with high SDK restrictions. The OS literally lied—it accepted the backup request, drew the UI prompt, and then silently aborted the stream right after writing the header.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempt 6: Forcing the Migration Engine
&lt;/h2&gt;

&lt;p&gt;If &lt;code&gt;adb backup&lt;/code&gt; is blocked, maybe the internal migration engine works. I went back into &lt;code&gt;rish&lt;/code&gt; to force the device's Backup Manager to dump the local transport payload.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bmgr transport com.android.localtransport/.LocalTransport
bmgr backupnow com.app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; &lt;code&gt;backup is not allowed&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Front door, back door, side door—all welded shut.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Grim Conclusion
&lt;/h2&gt;

&lt;p&gt;After exhausting WSL pipelines, Shizuku shells, Apktool manifesting, and ADB protocols, the reality set in. Google has officially killed non-root app data access.&lt;/p&gt;

&lt;p&gt;The rationale is "security." But when I purchase hardware, install an app, generate local data, and am systematically barred from moving a 1MB text file to a backup folder by the operating system, that isn't security. That is captivity.&lt;/p&gt;

&lt;p&gt;If a developer doesn't explicitly build an "Export Save" button into their UI, your data will die on that device. For power users and developers, the message from Android 16 is clear: &lt;strong&gt;Root your device, or accept that you are just a guest on your own hardware.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Has anyone else hit this wall on Android 16? Are there any obscure binder exploits or PC-side loopbacks left, or is Magisk officially the only way out? Let me know in the comments.&lt;/p&gt;

</description>
      <category>privacy</category>
      <category>android</category>
      <category>google</category>
      <category>data</category>
    </item>
    <item>
      <title>Why I Walked Back from Next.js and RSC to a Plain SPA and a Separate Backend</title>
      <dc:creator>Zul Ikram Musaddik Rayat</dc:creator>
      <pubDate>Thu, 21 May 2026 14:04:09 +0000</pubDate>
      <link>https://dev.to/devrayat000/why-i-walked-back-from-nextjs-and-rsc-to-a-plain-spa-and-a-separate-backend-3ibo</link>
      <guid>https://dev.to/devrayat000/why-i-walked-back-from-nextjs-and-rsc-to-a-plain-spa-and-a-separate-backend-3ibo</guid>
      <description>&lt;p&gt;I've been writing React for a long time, and I want to tell you a story about a round trip. It starts with me being a true believer in Next.js, runs through the era when the App Router and Server Components moved into my house and rearranged the furniture, passes through a couple of genuinely scary security incidents, and ends with me happily back on a boring, well-understood architecture: a single-page app and a separate backend that talk over a plain HTTP boundary.&lt;/p&gt;

&lt;p&gt;This isn't a "framework X is dead" hot take. It's a description of how my own thinking changed, and why the thing I reach for in 2026 looks a lot like the thing I would have reached for in 2018... except smarter about the parts that actually matter.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Next.js was genuinely great
&lt;/h2&gt;

&lt;p&gt;I want to be fair to Next.js, because for a long stretch it earned its reputation.&lt;/p&gt;

&lt;p&gt;The Pages Router era was a sweet spot. You had file-based routing that you could explain to a new hire in five minutes. You had &lt;code&gt;getStaticProps&lt;/code&gt; and &lt;code&gt;getServerSideProps&lt;/code&gt; - two functions, clearly named, with an obvious mental model: one runs at build time, the other runs per request, and everything else is just React. You got code splitting, image optimization, and a dev server that mostly worked. Deploying was a non-event.&lt;/p&gt;

&lt;p&gt;The thing I appreciated most was that the boundary between server and client was &lt;em&gt;legible&lt;/em&gt;. Data fetching happened in named lifecycle-ish functions at the top of a route. The component tree below them was ordinary client React. I could point at any line of code and tell you which machine it ran on. That legibility is worth more than people realized at the time, and we mostly only noticed it once it was gone.&lt;/p&gt;

&lt;p&gt;So when I say I left, understand that I left something I used to love.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then the App Router and Server Components moved in
&lt;/h2&gt;

&lt;p&gt;The App Router and React Server Components were pitched as the next evolution, and on paper the pitch is seductive: render components on the server, ship less JavaScript, colocate data fetching with the component that needs it, stream HTML to the browser as it becomes ready. Who doesn't want less JS and faster paint?&lt;/p&gt;

&lt;p&gt;In practice, here is what I actually experienced.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The mental model fractured.&lt;/strong&gt; Suddenly every file was either a Server Component or a Client Component, the distinction was load-bearing, and the boundary was declared with a &lt;code&gt;"use client"&lt;/code&gt; string at the top of a file. That directive is viral in one direction and silent about it. A component that was fine yesterday breaks today because something three levels up crossed the boundary, and the error you get is rarely "you crossed a boundary"; it's a serialization complaint, or a hydration mismatch, or a hook being called somewhere it can't be.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"It works in dev" stopped meaning anything.&lt;/strong&gt; Caching behavior between &lt;code&gt;next dev&lt;/code&gt;, &lt;code&gt;next build &amp;amp;&amp;amp; next start&lt;/code&gt;, and the production edge deployment diverged enough that I stopped trusting local results. I had bugs that only existed in the production cache. I had stale data that no &lt;code&gt;revalidate&lt;/code&gt; value seemed to fix. I learned more about the Next.js caching layers than I ever wanted to, and the reward for that knowledge was a permanent low-grade anxiety about whether any given page was actually fresh.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The server/client boundary stopped being legible.&lt;/strong&gt; This is the one that really got me. The thing I loved most about the Pages era, being able to look at code and know where it ran, was exactly the thing RSC took away. Data fetching is now scattered through the tree. &lt;code&gt;async&lt;/code&gt; components look like normal components but aren't. The boundary is real and consequential but invisible at the call site.&lt;/p&gt;

&lt;p&gt;None of these are unfixable in isolation. Together they meant I was spending a large fraction of my time fighting the framework's model instead of building the product. That's the signal I should have read sooner.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then the security incidents made it concrete
&lt;/h2&gt;

&lt;p&gt;Architectural friction is a judgment call. Security incidents are not, and a run of them turned my vague unease into a decision.&lt;/p&gt;

&lt;p&gt;It started with &lt;strong&gt;CVE-2025-29927&lt;/strong&gt;, the middleware authorization bypass disclosed in March 2025 at CVSS 9.1. The mechanism was almost embarrassingly simple: Next.js used the internal header &lt;code&gt;x-middleware-subrequest&lt;/code&gt; to mark subrequests and prevent infinite middleware loops, but the header was never meant to be client-controlled, and by spoofing it, attackers could skip middleware entirely, auth and authorization included, and reach protected routes with one crafted &lt;code&gt;curl&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If that had been a one-off, I'd have shrugged it off; every framework has bugs. But it wasn't a one-off. The back half of 2025 and the start of 2026 brought a steady drumbeat, and the pattern is what mattered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CVE-2025-55182&lt;/strong&gt; (December 2025) - a React Server Components flaw rated &lt;strong&gt;CVSS 10.0&lt;/strong&gt;, the maximum. It sat in the &lt;code&gt;react-server-dom-*&lt;/code&gt; packages and therefore affected every framework built on RSC &amp;amp; Next.js, React Router's RSC APIs, Waku, Parcel's RSC plugin, and the Vite RSC plugin. A perfect score is rare, and this one lived in the RSC machinery itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CVE-2026-23864&lt;/strong&gt; (January 2026, CVSS 7.5) - a denial-of-service flaw in React Server Components: a malicious payload sent to a Server Function endpoint triggers memory exhaustion or runaway CPU. Patched alongside two more medium-severity Next.js CVEs in the same release.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CVE-2026-23869&lt;/strong&gt; and &lt;strong&gt;CVE-2026-23870&lt;/strong&gt; (early 2026) - more DoS issues in Server Components, triggered by specially crafted HTTP requests to App Router Server Function endpoints that blow up CPU on deserialization. Affected Next.js 13.x through 16.x on the App Router.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CVE-2026-44578&lt;/strong&gt; (May 2026, CVSS 7.8) - a server-side request forgery in the self-hosted Next.js Node server: crafted WebSocket upgrade requests let an attacker proxy requests to arbitrary internal destinations, including cloud metadata endpoints.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Look at where these clusters are. A middleware bypass, then a string of Server Component and Server Function flaws, several of them triggered specifically &lt;em&gt;on deserialization of a crafted request&lt;/em&gt;. This isn't bad luck distributed randomly across a codebase. The vulnerable surface is the server-rendering and server-function machinery, the exact part of the stack that the App Router and RSC made central. The more your framework does on the server on every request, the more attack surface you have signed up for, and a 10.0 in the shared RSC packages means a single bug ripples across every framework that adopted the model.&lt;/p&gt;

&lt;p&gt;What unsettled me wasn't any single CVE... it was what the cluster &lt;em&gt;implied about the architecture&lt;/em&gt;. Teams like mine had been quietly nudged to treat middleware as the authorization layer, because in the App Router model, auth-in-middleware is the path of least resistance. The level-headed takeaway in every writeup was the one that stung: middleware should supplement, not replace, security enforced closer to the data. So, I sat with a question: how much of my security posture do I want coupled to a rendering framework's internal request plumbing? My answer was "as little as possible."&lt;/p&gt;

&lt;h2&gt;
  
  
  The TanStack route - also not a free lunch
&lt;/h2&gt;

&lt;p&gt;When developers get fed up with Next.js, a common next stop is the TanStack ecosystem - TanStack Router and TanStack Start. And I'll be honest: TanStack Router is a genuinely nice piece of engineering. The type-safe routing is best-in-class, the search-param handling is thoughtful, and the data loading is well-considered. If you'd asked me purely about developer experience, I'd have said good things.&lt;/p&gt;

&lt;p&gt;But "switch to TanStack" is not the escape hatch some people think it is, and recent history makes that point sharply.&lt;/p&gt;

&lt;p&gt;In May 2026, the TanStack npm packages were hit by a supply chain attack. On 11 May 2026, a threat group ran a coordinated supply chain attack against the npm and PyPI ecosystems, compromising packages across multiple namespaces, including the &lt;code&gt;@tanstack&lt;/code&gt; namespace, which contains &lt;code&gt;@tanstack/react-router&lt;/code&gt;, one of the most widely-used routing libraries in the React ecosystem, with roughly 12 million weekly downloads. Between 19:20 and 19:26 UTC on a single Monday, the attacker published 84 malicious versions across 42 TanStack packages.&lt;/p&gt;

&lt;p&gt;The payload was not subtle. It targeted CI/CD tokens, cloud credentials across AWS, GCP, and Azure, Kubernetes service accounts, Vault, and registry tokens, and it used stolen npm and GitHub Actions tokens to publish poisoned versions of more packages, functioning as a worm spreading through the npm ecosystem. It also installed a persistent daemon that polled GitHub every 60 seconds, and on detecting token revocation would attempt to run &lt;code&gt;rm -rf&lt;/code&gt; on the user's home directory. One detail makes this especially grim: the compromised packages carried valid SLSA Build Level 3 provenance attestations, making it the first documented npm worm to produce validly attested malicious packages because the malicious versions were published through the project's own GitHub Actions release pipeline using hijacked OIDC tokens.&lt;/p&gt;

&lt;p&gt;I want to be careful and fair here. This was &lt;strong&gt;not&lt;/strong&gt; a flaw in TanStack Router's code. The attackers got in by forking a TanStack repository on GitHub and submitting a malicious commit under a fabricated identity. The TanStack maintainers responded quickly and communicated well. This could have happened, and has happened, to almost anyone - 2025 saw significant growth in supply chain attacks, and the npm ecosystem, because of its popularity, absorbed a large share of them. The September 2025 Shai-Hulud worm was the first self-propagating worm in the npm ecosystem and affected over 500 packages.&lt;/p&gt;

&lt;p&gt;So the lesson I drew from the TanStack incident isn't "TanStack is unsafe." It's the opposite of tribal. The lesson is: &lt;strong&gt;no framework choice immunizes you from supply chain risk, and the more of your stack you concentrate into one heavyweight meta-framework, the larger and more attractive a single compromise becomes.&lt;/strong&gt; Switching framework brand doesn't fix that. Reducing surface area and dependency count does.&lt;/p&gt;

&lt;h2&gt;
  
  
  The deeper reason full-stack RSC was always going to be hard: serialization
&lt;/h2&gt;

&lt;p&gt;Set the CVEs aside for a moment, because I think there's a more fundamental issue, and it's the one that finally settled my thinking.&lt;/p&gt;

&lt;p&gt;The whole promise of Server Components, server actions, and streaming is that the server/client boundary should feel seamless... You just write components and call functions, and the framework figures out what runs where. But a function call across a network is not a function call. The boundary is real, and it is made of &lt;strong&gt;serialization&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Everything that crosses from server to client has to be serialized into the RSC payload, streamed, and deserialized. That constraint quietly shapes everything:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You can't pass a function to a Client Component from a Server Component unless it's a server action, because functions don't serialize. So "just pass a callback", the most ordinary thing in React, becomes a boundary decision.&lt;/li&gt;
&lt;li&gt;Class instances, &lt;code&gt;Date&lt;/code&gt; semantics, &lt;code&gt;Map&lt;/code&gt;/&lt;code&gt;Set&lt;/code&gt;, anything with methods or identity, all of it has to be flattened to plain data, or it doesn't make the trip cleanly.&lt;/li&gt;
&lt;li&gt;Errors that originate on the server are serialized before you see them on the client, so the stack trace you get is often a translation of the real problem rather than the problem itself.&lt;/li&gt;
&lt;li&gt;Streaming adds &lt;em&gt;time&lt;/em&gt; as a dimension. Components resolve out of order, Suspense boundaries flush independently, and now you're reasoning about the partial states of a tree mid-stream, which is a genuinely harder mental model than "fetch, then render."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This isn't a bug anyone can patch. It's intrinsic. The instant you let a UI tree straddle a network boundary, you have signed up for a distributed-systems problem dressed in component clothing, and serialization is where that problem leaks through. RSC's seamlessness is, to a real degree, a leaky abstraction over an inherently unseamless thing. You can build impressive demos on it. Holding it stable across a large app, a team, and a year of changes is a different sport.&lt;/p&gt;

&lt;p&gt;And notice the connection back to that CVE cluster. Several of those 2026 flaws, the Server Function DoS bugs, trigger &lt;em&gt;on deserialization of a crafted request&lt;/em&gt;. That is not a coincidence. Deserializing untrusted input into a live program is one of the oldest, most dangerous operations in computing, and the RSC model puts a deserialization boundary on the hot path of ordinary feature work. The architecture didn't just make my app harder to reason about; it made the framework itself a richer target. Serialization is both the ergonomic tax and the security surface, the same seam, charged twice.&lt;/p&gt;

&lt;p&gt;Once I framed it that way, my decision basically made itself. I don't want my UI framework and my data boundary fused. I want the network boundary to be &lt;strong&gt;explicit, visible, and boring&lt;/strong&gt;, a place I deliberately walk up to, not a seam hidden inside my component tree.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack I actually use now
&lt;/h2&gt;

&lt;p&gt;So here's where I landed. None of it is exotic. That's the point.&lt;/p&gt;

&lt;h3&gt;
  
  
  Frontend: React Router in framework mode, on Vite, mostly SPA
&lt;/h3&gt;

&lt;p&gt;I use &lt;strong&gt;React Router in its framework mode&lt;/strong&gt; with &lt;strong&gt;Vite&lt;/strong&gt;. Framework mode gives me the things I genuinely missed from Next.js file-based-ish routing conventions, loaders and actions, nested layouts, type-safe params without forcing a server-rendering model on me.&lt;/p&gt;

&lt;p&gt;The crucial part: &lt;strong&gt;I don't run SSR.&lt;/strong&gt; A handful of pages that are truly static marketing, docs, and the landing page get &lt;strong&gt;prerendered&lt;/strong&gt; at build time, so they ship as fast static HTML and are friendly to crawlers. Everything else is a &lt;strong&gt;plain client-rendered SPA&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;People will say, "But SPAs are slow/bad for SEO / out of fashion." For an authenticated application, a dashboard, a tool, or a product behind a login, almost none of that critique applies. There's nothing for Google to index behind the login wall. The first paint is a thin shell, the bundle is code-split per route, and from then on, navigation is instant because it's all client-side. I get a build artifact that is just static files. I can put it on any CDN or object store. There is no server-rendering process to keep alive, patch, scale, or get a CVE in. The deployment story is "upload a folder," and the security surface of the frontend collapses to almost nothing.&lt;/p&gt;

&lt;p&gt;And critically: there is &lt;strong&gt;no serialization boundary inside my UI&lt;/strong&gt;. The component tree runs entirely in the browser. It talks to the backend through &lt;code&gt;fetch&lt;/code&gt;, against an API I designed on purpose. The boundary is visible in the code, at exactly the lines where it exists.&lt;/p&gt;

&lt;p&gt;Here's what that looks like in practice. A route loads its data in a &lt;code&gt;clientLoader&lt;/code&gt; which, despite living in a framework-mode route file, runs entirely in the browser and just calls my API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/routes/projects.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Route&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;./+types/projects&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;api&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;~/lib/api-client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;clientLoader&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ClientLoaderArgs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Runs in the browser. No server. Just an HTTP call to my backend.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;projects&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;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;projects&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="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;projects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&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;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Failed to load&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;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;502&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;projects&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;projects&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="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Projects&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;loaderData&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ComponentProps&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ul&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;loaderData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;li&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/li&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="p"&gt;))}&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/ul&lt;/span&gt;&lt;span class="err"&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;There's no &lt;code&gt;loader&lt;/code&gt; (server) here at all, only &lt;code&gt;clientLoader&lt;/code&gt;. The seam is the &lt;code&gt;api.projects.$get()&lt;/code&gt; call, and I can point at it. Auth is enforced the same way, with a &lt;code&gt;clientMiddleware&lt;/code&gt; that runs before the loaders on protected routes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/routes/dashboard.tsx - a protected layout route&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;redirect&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-router&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="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Route&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;./+types/dashboard&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;api&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;~/lib/api-client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;clientMiddleware&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ClientMiddlewareFunction&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="k"&gt;async &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="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;res&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;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;me&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="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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/login&lt;/span&gt;&lt;span class="dl"&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="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userContext&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;res&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="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;But notice what this middleware &lt;em&gt;is not&lt;/em&gt;: it is not my security boundary. It's a UX convenience; it bounces an unauthenticated user to the login screen so they don't stare at a broken page. If someone skips it entirely, they gain nothing, because &lt;strong&gt;every API endpoint enforces auth itself, server-side, on every request.&lt;/strong&gt; This is the whole lesson of CVE-2025-29927 applied: client-side middleware is for experience, never for enforcement. The frontend can't bypass a check that the backend owns.&lt;/p&gt;

&lt;p&gt;The backend side of that contract, in Hono, is explicit and inspectable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// server/index.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Hono&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hono&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;cors&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hono/cors&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;csrf&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;hono/csrf&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;authMiddleware&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;./auth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Hono&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="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="nf"&gt;cors&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://app.example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;csrf&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://app.example.com&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;// Real enforcement: the API itself rejects unauthenticated requests.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;projects&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;Hono&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;authMiddleware&lt;/span&gt;&lt;span class="p"&gt;)&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="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;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="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;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="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="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="c1"&gt;// set by authMiddleware, trusted here&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;projectsForUser&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="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;routes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/projects&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/auth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;authRoutes&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;type&lt;/span&gt; &lt;span class="nx"&gt;AppType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// &amp;lt;- exported for the frontend&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last line is the only reason I reach for Hono specifically. Exporting &lt;code&gt;AppType&lt;/code&gt; lets the frontend build a fully typed client with no codegen step and no shared runtime:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/lib/api-client.ts&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;hc&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;hono/client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AppType&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;../../server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// `api` is end-to-end typed against the backend routes.&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;api&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hc&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AppType&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;/api&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;init&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;include&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the backend changes a route's shape, the frontend stops compiling. That's the type safety people credit good Next.js setups with, and I keep it without fusing the two halves into one runtime. Swap Hono for Go or Python, and I lose only this typed-client convenience; the architecture is unchanged, because the contract is just HTTP.&lt;/p&gt;

&lt;p&gt;For data that loads &lt;em&gt;after&lt;/em&gt; the initial render, a slow widget on an otherwise-ready page, I reach for &lt;code&gt;Suspense&lt;/code&gt; and the &lt;code&gt;use&lt;/code&gt; hook. This is the one genuinely good idea from the streaming era, and the nice part is it works perfectly well in a plain SPA, with no server rendering involved:&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;Suspense&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;use&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;api&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;~/lib/api-client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Kick off the request; do NOT await it. `use` will suspend on the promise.&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ActivityFeed&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;feedPromise&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;feedPromise&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;Activity&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;activity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;feedPromise&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// suspends until resolved&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Feed&lt;/span&gt; &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;activity&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Dashboard&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Promise created during render, in the browser. No RSC payload.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;feedPromise&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useMemo&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;activity&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="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&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;r&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="p"&gt;[]);&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Header&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Suspense&lt;/span&gt; &lt;span class="na"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FeedSkeleton&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ActivityFeed&lt;/span&gt; &lt;span class="na"&gt;feedPromise&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;feedPromise&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Suspense&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&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;Same &lt;code&gt;Suspense&lt;/code&gt;, same &lt;code&gt;use&lt;/code&gt;, same streaming-feel UX of content arriving progressively, but the promise is an ordinary &lt;code&gt;fetch&lt;/code&gt; resolving in the browser, not a chunk of a serialized server tree. There's no out-of-order flush to reason about, no hydration boundary, no payload format. I got the ergonomic win of the streaming era and left behind the serialized-tree machinery that came bundled with it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Backend: a separate service - Hono if I want TypeScript
&lt;/h3&gt;

&lt;p&gt;The backend is its own thing, deployed, scaled, and reasoned about independently. Right now, I mostly reach for &lt;strong&gt;Hono&lt;/strong&gt;, because it's small, fast, runs anywhere from Node to workers to Bun, and lets me keep TypeScript end to end. With Hono, I can share types between client and server and even get a typed client, so I lose none of the type safety that good Next.js setups gave me.&lt;/p&gt;

&lt;p&gt;But, and this matters &lt;strong&gt;the backend doesn't have to be JavaScript at all.&lt;/strong&gt; It could just as easily be Go or Python. The reason that's now a free choice is precisely the serialization point from earlier: when your frontend is an SPA talking to an API over plain HTTP and JSON, there is no RSC payload, no server-action boundary, no shared-component-tree contract that forces both sides to be the same language. The contract is just HTTP. Go is fantastic for a backend. Python is fantastic for a backend. I use Hono specifically and only when I want the TypeScript type-sharing convenience; it's a preference, not a requirement. The architecture itself is language-agnostic, and that flexibility is a feature I gave myself by &lt;em&gt;removing&lt;/em&gt; the fused boundary.&lt;/p&gt;

&lt;h3&gt;
  
  
  Security lives in the backend, on purpose
&lt;/h3&gt;

&lt;p&gt;This is the part I care about most, and it's the direct lesson of CVE-2025-29927. &lt;strong&gt;Authorization and security are the backend's job, enforced at the API layer, every request, no exceptions.&lt;/strong&gt; Not in framework middleware. Not coupled to a renderer's internal request plumbing. In the service that owns the data.&lt;/p&gt;

&lt;p&gt;Concretely, on the backend, I run:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Authentication&lt;/strong&gt; as a real, deliberate layer of sessions or tokens, validated server-side on every protected request. The server, which owns the database, decides what you can see. The frontend is never trusted to enforce this; it only reflects it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CORS&lt;/strong&gt; configured tightly - an explicit allowlist of origins, not a wildcard, so the browser only lets my actual frontend talk to the API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CSRF protection&lt;/strong&gt; - since a SPA-plus-API setup often uses cookies, I use proper anti-CSRF tokens and/or &lt;code&gt;SameSite&lt;/code&gt; cookie attributes so a malicious site can't ride a logged-in user's session.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CAPTCHA&lt;/strong&gt; on the abuse-prone endpoints, signup, login, password reset, public form submissions to keep bots and credential-stuffing out.&lt;/li&gt;
&lt;li&gt;Plus the usual unglamorous hygiene: rate limiting, strict input validation at the API edge, security headers, secrets kept out of the client bundle, and least-privilege everywhere.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every one of these is something I configure and can point at. There's no invisible header that flips my auth off, because my auth was never a header; it's a checked condition in the request handler that sits between the attacker and the database. If someone bypasses a routing layer, they hit the API, and the API still says no.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually gained
&lt;/h2&gt;

&lt;p&gt;Stepping back, here's the trade I made and why I'm happy with it.&lt;/p&gt;

&lt;p&gt;I gave up: server-rendered dynamic pages out of the box, and the "it's all one project" convenience of a meta-framework.&lt;/p&gt;

&lt;p&gt;I got back, in exchange:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A legible architecture.&lt;/strong&gt; I can point at any line and say which machine it runs on. The network boundary is one explicit place where the API, instead of being smeared invisibly through a component tree.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No serialization tax.&lt;/strong&gt; The UI tree lives entirely in the browser. No RSC payload, no server-action plumbing, no streaming-order puzzles.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A tiny frontend attack surface.&lt;/strong&gt; Static files on a CDN. Nothing to keep running, nothing to patch, no Server Functions to send crafted payloads at, nothing to catch the next 10.0 in the RSC packages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security I can see.&lt;/strong&gt; Auth, CORS, CSRF, CAPTCHA, and rate limiting all live in one service, enforced per request, decoupled from any renderer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend freedom.&lt;/strong&gt; Hono today for the TypeScript ergonomics; Go or Python tomorrow if a project wants them. The HTTP contract doesn't care.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Smaller blast radius.&lt;/strong&gt; Two modest, separately-updated pieces with fewer heavyweight dependencies, instead of one large meta-framework whose single compromise is a very attractive target.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  So, is Next.js bad? Is TanStack bad?
&lt;/h2&gt;

&lt;p&gt;No. I want to end honestly, because tribal blog posts age badly.&lt;/p&gt;

&lt;p&gt;Next.js and TanStack are both serious, well-engineered projects built by talented people. The CVEs and the supply chain attack I described are real and worth knowing about, but the CVEs got patched, and the TanStack incident was an account-and-pipeline compromise that could have hit almost any popular package. If your product genuinely needs server rendering for SEO on dynamic content, or you have a content-heavy site where streaming pays for itself, a meta-framework can absolutely be the right call. Plenty of teams ship great things on exactly the stack I walked away from.&lt;/p&gt;

&lt;p&gt;My point is narrower and more personal. For the apps &lt;strong&gt;I&lt;/strong&gt; build, authenticated products where the UI is interactive and the data is the crown jewels, the full-stack RSC model added a serialization-shaped boundary I didn't want, hid the seam I most needed to see, and nudged my security toward the framework's plumbing instead of my own backend. A SPA plus a separate, well-secured backend gave me a smaller, more legible, more boring system. And in software, boring is usually a compliment.&lt;/p&gt;

&lt;p&gt;That's the round trip. I left, I looked around, and I came back to a plain SPA and a real backend, older ideas, held with sharper reasons.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>nextjs</category>
      <category>react</category>
    </item>
    <item>
      <title>Email Verification with Better-Auth (Basics Tutorial, Ep. 2)</title>
      <dc:creator>Zul Ikram Musaddik Rayat</dc:creator>
      <pubDate>Sun, 21 Sep 2025 16:27:58 +0000</pubDate>
      <link>https://dev.to/devrayat000/email-verification-with-better-auth-basics-tutorial-ep-2-3mm3</link>
      <guid>https://dev.to/devrayat000/email-verification-with-better-auth-basics-tutorial-ep-2-3mm3</guid>
      <description>&lt;p&gt;Welcome back to the &lt;a href="https://www.youtube.com/playlist?list=PLKNCWppgn6elkj28NRedKURDMAp-cuIJm" rel="noopener noreferrer"&gt;&lt;strong&gt;Better-Auth Basics&lt;/strong&gt;&lt;/a&gt; series! 🚀  &lt;/p&gt;

&lt;p&gt;In &lt;a href="https://dev.to/devrayat000/authentication-using-better-auth-basics-tutorial-1bc1"&gt;Episode 1&lt;/a&gt;, we set up &lt;strong&gt;Better-Auth&lt;/strong&gt; with Drizzle ORM and implemented sign-up and login functionality. It worked great… but there’s a big flaw:  &lt;/p&gt;

&lt;p&gt;👉 Anyone can sign up with &lt;strong&gt;any random email&lt;/strong&gt; — even one they don’t own.  &lt;/p&gt;

&lt;p&gt;That’s obviously not safe for production. So in this post, we’ll fix that by adding &lt;strong&gt;Email Verification&lt;/strong&gt; with Better-Auth.  &lt;/p&gt;




&lt;h2&gt;
  
  
  ❌ The Problem Without Verification
&lt;/h2&gt;

&lt;p&gt;Right now, a user can type &lt;strong&gt;any email address&lt;/strong&gt; on the registration page, and the server will happily accept it.  &lt;/p&gt;

&lt;p&gt;That means fake accounts, spam signups, and security risks. We need a way to make sure the person &lt;strong&gt;actually owns the email&lt;/strong&gt; they’re registering with.  &lt;/p&gt;




&lt;h2&gt;
  
  
  ✅ Email Verification with Better-Auth
&lt;/h2&gt;

&lt;p&gt;The good news? Better-Auth already has &lt;strong&gt;email verification built in&lt;/strong&gt; for email/password authentication. We just need to configure it. Let’s go step by step.  &lt;/p&gt;




&lt;h2&gt;
  
  
  🛠 Step 1: Configure an Email Provider
&lt;/h2&gt;

&lt;p&gt;For sending verification emails, I used &lt;a href="https://resend.com/" rel="noopener noreferrer"&gt;Resend&lt;/a&gt;.  &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a Resend account
&lt;/li&gt;
&lt;li&gt;Add your custom domain
&lt;/li&gt;
&lt;li&gt;Generate an API key
&lt;/li&gt;
&lt;li&gt;Save it in your &lt;code&gt;.env&lt;/code&gt; file:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;RESEND_API_KEY&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;your-api-key&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🛠 Step 2: Create Email Templates
&lt;/h2&gt;

&lt;p&gt;Inside Resend, configure simple templates for your verification emails.&lt;br&gt;
This is the message your users will see in their inbox with the verification link.&lt;/p&gt;


&lt;h2&gt;
  
  
  🛠 Step 3: Enable Email Verification in Better-Auth
&lt;/h2&gt;

&lt;p&gt;In your Better-Auth configuration, enable the verification features:&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;betterAuth&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;better-auth/server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;schema&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;../db/schema&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;auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;betterAuth&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="na"&gt;emailAndPassword&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;enabled&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;requireEmailVerification&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;emailVerification&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;sendOnSignUp&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;autoSignInAfterVerification&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="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;sendVerificationEmail&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="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sendVerificationEmail&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;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="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;
  
  
  🛠 Step 4: Send Verification Email on Sign Up
&lt;/h2&gt;

&lt;p&gt;Better-Auth will now automatically send a verification email when a new user registers.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;After sign-up, redirect the user to a verify page in your app.&lt;/li&gt;
&lt;li&gt;Tell them to check their inbox for the verification link.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/verify/page.tsx&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;VerifyPage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Verify Your Email&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        We’ve sent you a link. Please check your inbox and click it to activate your account.
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🛠 Step 5: Testing the Flow
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Register with a real email address&lt;/li&gt;
&lt;li&gt;Check your inbox → you’ll see the verification email&lt;/li&gt;
&lt;li&gt;Click the link → the server validates it&lt;/li&gt;
&lt;li&gt;You’re automatically signed in and redirected to the homepage 🎉&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here’s what the successful response looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Email verified and user signed in"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🎉 And That’s It!
&lt;/h2&gt;

&lt;p&gt;With just a few lines of configuration, we added secure email verification to our Better-Auth setup.&lt;/p&gt;

&lt;p&gt;Now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Users must prove they own their email&lt;/li&gt;
&lt;li&gt;Fake signups are blocked&lt;/li&gt;
&lt;li&gt;Onboarding is safer and more production-ready&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  📌 What’s Next?
&lt;/h2&gt;

&lt;p&gt;This was Episode 2 of Better-Auth Basics.&lt;br&gt;
In future episodes, we’ll cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;👥 Role-based authentication&lt;/li&gt;
&lt;li&gt;⚡ Rate limiting&lt;/li&gt;
&lt;li&gt;🛡 Middleware for protecting routes
Stay tuned — we’re just getting started.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🔗 Stay Connected
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;📽️ Youtube: &lt;a href="https://www.youtube.com/@code_unhinged" rel="noopener noreferrer"&gt;code_unhinged&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🐦 Twitter (X): &lt;a href="https://x.com/zul_rayat" rel="noopener noreferrer"&gt;zul_rayat&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 GitHub: &lt;a href="https://x.com/zul_rayat" rel="noopener noreferrer"&gt;devrayat000&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💼 LinkedIn: &lt;a href="https://www.linkedin.com/in/zim-rayat" rel="noopener noreferrer"&gt;zim-rayat&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📷 Instagram: &lt;a href="https://www.instagram.com/rayatttttttt/" rel="noopener noreferrer"&gt;rayatttttttt&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📘 Facebook: &lt;a href="https://www.facebook.com/rayat.ass" rel="noopener noreferrer"&gt;rayat.ass&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;💬 Got questions? Drop them in the comments — I reply to every one!&lt;br&gt;
👍 Don’t forget to like, share, and subscribe for more dev content.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>javascript</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>Authentication Using Better-Auth (Basics Tutorial)</title>
      <dc:creator>Zul Ikram Musaddik Rayat</dc:creator>
      <pubDate>Wed, 17 Sep 2025 00:04:40 +0000</pubDate>
      <link>https://dev.to/devrayat000/authentication-using-better-auth-basics-tutorial-1bc1</link>
      <guid>https://dev.to/devrayat000/authentication-using-better-auth-basics-tutorial-1bc1</guid>
      <description>&lt;p&gt;What’s up, devs! 👋&lt;br&gt;
In this post, I’ll walk you through &lt;strong&gt;setting up authentication in a Node.js/Next.js project using Better-Auth&lt;/strong&gt; — a powerful new authentication library. This is based on the first episode of my YouTube playlist &lt;strong&gt;&lt;a href="https://www.youtube.com/playlist?list=PLKNCWppgn6elkj28NRedKURDMAp-cuIJm" rel="noopener noreferrer"&gt;Better-Auth Basics&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Better-Auth makes it really easy to implement secure authentication with features like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Email + Password authentication&lt;/li&gt;
&lt;li&gt;✅ Social sign-ins&lt;/li&gt;
&lt;li&gt;✅ Built-in rate limiting&lt;/li&gt;
&lt;li&gt;✅ Automatic database management &amp;amp; adapters&lt;/li&gt;
&lt;li&gt;✅ Two-factor authentication support&lt;/li&gt;
&lt;li&gt;✅ Simple client API for frontend integrations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sounds exciting? Let’s dive in! 🚀&lt;/p&gt;
&lt;h2&gt;
  
  
  🛠 Project Setup
&lt;/h2&gt;

&lt;p&gt;For this tutorial, I’m working on my personal project — a ride-sharing app.&lt;br&gt;
We’ll integrate Better-Auth into it step by step.&lt;/p&gt;
&lt;h3&gt;
  
  
  1. Install Dependencies
&lt;/h3&gt;

&lt;p&gt;We’ll need Better-Auth, Drizzle ORM, and Postgres.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bun add better-auth drizzle-orm postgres
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Environment Variables
&lt;/h3&gt;

&lt;p&gt;Generate a secret key and add it to your .env file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;BETTER_AUTH_SECRET&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;your-secret-key&lt;/span&gt;
&lt;span class="py"&gt;BETTER_AUTH_URL&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;http://localhost:3000 # Base URL of your app&lt;/span&gt;
&lt;span class="py"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;postgresql://user:password@localhost:5432/dbname&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  ⚙️ Database Setup with Drizzle
&lt;/h2&gt;

&lt;p&gt;Better-Auth ships with ready-to-use schemas.&lt;br&gt;
We’ll copy those into our project and run migrations.&lt;/p&gt;
&lt;h3&gt;
  
  
  1. Generate Schema
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bunx @better-auth/cli generate &lt;span class="c"&gt;# create better-auth schema&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  2. Run Migrations
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bunx drizzle-kit generate &lt;span class="c"&gt;# generate migration files&lt;/span&gt;
bunx drizzle-kit migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Once done, your database will have all the required tables.&lt;/p&gt;
&lt;h2&gt;
  
  
  🔑 Enable Email/Password Authentication
&lt;/h2&gt;

&lt;p&gt;Inside your &lt;strong&gt;Better-Auth server setup&lt;/strong&gt;, enable email/password auth:&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="c1"&gt;// lib/auth.ts&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;betterAuth&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;better-auth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;drizzleAdapter&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;better-auth/adapters/drizzle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;db&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;@/db&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// your drizzle instance&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;schema&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;@/db/schema&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// your drizzle schema&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;auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;betterAuth&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;drizzleAdapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// or "mysql", "sqlite"&lt;/span&gt;
    &lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;emailAndPassword&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🖥 Setting Up the Client
&lt;/h2&gt;

&lt;p&gt;Better-Auth provides a client utility to make frontend integration simple.&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="c1"&gt;// lib/auth-client.ts&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;createAuthClient&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;better-auth/react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;authClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createAuthClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="cm"&gt;/** The base URL of the server (optional if you're using the same domain) */&lt;/span&gt;
  &lt;span class="na"&gt;baseURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http://localhost:3000&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  📡 Next.js API Routes
&lt;/h2&gt;

&lt;p&gt;Now, let’s connect it with Next.js routes.&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="c1"&gt;// app/api/auth/[...all]/route.ts&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;auth&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;@/lib/auth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;toNextJsHandler&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;better-auth/next-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;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;POST&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;toNextJsHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This route will handle all sign-in, sign-up, and session management APIs.&lt;/p&gt;

&lt;h2&gt;
  
  
  👤 Implement Sign-Up
&lt;/h2&gt;

&lt;p&gt;On your &lt;strong&gt;register page&lt;/strong&gt;, use the client:&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;handleRegister&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;authClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signUp&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="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;callbackUrl&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="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🔐 Implement Sign-In
&lt;/h2&gt;

&lt;p&gt;Similarly, on your login page:&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;handleLogin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;authClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signIn&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="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;callbackUrl&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="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🎉 Testing It Out
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Tried signing up → User was created successfully ✅&lt;/li&gt;
&lt;li&gt;Tried logging in → Redirected to homepage with 200 response ✅&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s it! We now have a working authentication flow with Better-Auth.&lt;/p&gt;

&lt;h2&gt;
  
  
  📌 What’s Next?
&lt;/h2&gt;

&lt;p&gt;This tutorial only covers the basics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Setting up Better-Auth&lt;/li&gt;
&lt;li&gt;Running database migrations with Drizzle&lt;/li&gt;
&lt;li&gt;Implementing Sign-In &amp;amp; Sign-Up&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In upcoming posts/videos, I’ll cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔑 Authorization &amp;amp; Role-Based Access&lt;/li&gt;
&lt;li&gt;⚡ Rate Limiting&lt;/li&gt;
&lt;li&gt;🔒 Two-Factor Authentication&lt;/li&gt;
&lt;li&gt;🔗 Social Logins&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stay tuned!&lt;/p&gt;

&lt;h2&gt;
  
  
  🔗 Connect With Me
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🐦 &lt;a href="https://www.facebook.com/rayat.ass" rel="noopener noreferrer"&gt;Facebook&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 &lt;a href="https://github.com/devrayat000" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💼 &lt;a href="https://www.linkedin.com/in/zim-rayat" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📽️ &lt;a href="https://www.youtube.com/@code_unhinged" rel="noopener noreferrer"&gt;Youtube&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 If you found this useful, drop a comment and let me know what you’re building with Better-Auth.&lt;br&gt;
And don’t forget to &lt;strong&gt;follow me here on DEV.to + subscribe on YouTube&lt;/strong&gt; for more tutorials.&lt;/p&gt;

&lt;p&gt;Happy coding! ✨&lt;/p&gt;

</description>
      <category>betterauth</category>
      <category>nextjs</category>
      <category>drizzle</category>
      <category>fullstack</category>
    </item>
    <item>
      <title>🍳 Recipe Generator – What’s in Your Pantry?</title>
      <dc:creator>Zul Ikram Musaddik Rayat</dc:creator>
      <pubDate>Sun, 14 Sep 2025 20:33:41 +0000</pubDate>
      <link>https://dev.to/devrayat000/recipe-generator-whats-in-your-pantry-4lnk</link>
      <guid>https://dev.to/devrayat000/recipe-generator-whats-in-your-pantry-4lnk</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/google-ai-studio-2025-09-03"&gt;Google AI Studio Multimodal Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;Have a few random ingredients lying around but no idea what to cook?&lt;br&gt;
&lt;strong&gt;Recipe Generator&lt;/strong&gt; helps you turn everyday pantry items into delicious meals.&lt;/p&gt;

&lt;p&gt;Simply type or select ingredients you already have, and the app instantly generates recipe ideas — complete with images, instructions, and serving tips.&lt;/p&gt;

&lt;p&gt;This makes meal planning fun, reduces food waste, and gives home cooks an easy way to experiment with new dishes.&lt;/p&gt;

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

&lt;p&gt;🔗 &lt;a href="https://gemini-recipe-generator-134638118864.us-west1.run.app" rel="noopener noreferrer"&gt;Live Demo Link&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Screenshots
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv31rpqhwql48uguuhs6v.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%2Fv31rpqhwql48uguuhs6v.png" alt="Intro"&gt;&lt;/a&gt;&lt;br&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%2Fwqtp1zuckyy5n4v7wqci.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%2Fwqtp1zuckyy5n4v7wqci.png" alt="Start Generating"&gt;&lt;/a&gt;&lt;br&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%2Fwey01oiqroyby5m1k7kn.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%2Fwey01oiqroyby5m1k7kn.png" alt="Recipe 1"&gt;&lt;/a&gt;&lt;br&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%2Fivgp7t51rh1awoirklvt.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%2Fivgp7t51rh1awoirklvt.png" alt="Recipe 2"&gt;&lt;/a&gt;&lt;br&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%2Fvf3aozsbq1tahp32vgxh.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%2Fvf3aozsbq1tahp32vgxh.png" alt="Recipe 3"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Used Google AI Studio
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Built the app on &lt;strong&gt;Google AI Studio&lt;/strong&gt;, deployed via &lt;strong&gt;Cloud Run&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Used Gemini’s &lt;strong&gt;multimodal capabilities&lt;/strong&gt; for:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Text understanding&lt;/strong&gt;: Parsing ingredient inputs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image generation&lt;/strong&gt;: Producing realistic dish images that match the recipe.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recipe generation&lt;/strong&gt;: Turning ingredient lists into step-by-step cooking instructions.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Multimodal Features
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ingredient → Recipe (Text)&lt;/strong&gt;: Natural language input is converted into structured recipes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recipe → Image (Visual)&lt;/strong&gt;: Each generated recipe is paired with a dish image for inspiration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Instructional Output&lt;/strong&gt;: Clear step-by-step cooking instructions with serving notes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Practical impact: Helps reduce food waste by suggesting meals from what you already own.&lt;/li&gt;
&lt;li&gt;User delight: A fun, interactive way to explore cooking ideas.&lt;/li&gt;
&lt;li&gt;Strong multimodal showcase: Combines text understanding, recipe creation, and visual generation in a single workflow.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Team
&lt;/h2&gt;

&lt;p&gt;Solo submission by &lt;a class="mentioned-user" href="https://dev.to/devrayat000"&gt;@devrayat000&lt;/a&gt; &lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>googleaichallenge</category>
      <category>ai</category>
      <category>gemini</category>
    </item>
    <item>
      <title>Hairstyle AI Try-On ✂️🤖</title>
      <dc:creator>Zul Ikram Musaddik Rayat</dc:creator>
      <pubDate>Sun, 14 Sep 2025 19:55:25 +0000</pubDate>
      <link>https://dev.to/devrayat000/hairstyle-ai-try-on-4p0e</link>
      <guid>https://dev.to/devrayat000/hairstyle-ai-try-on-4p0e</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/google-ai-studio-2025-09-03"&gt;Google AI Studio Multimodal Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;What if, before getting a haircut, you could actually see how you’d look?&lt;br&gt;
&lt;strong&gt;Hairstyle AI Try-On&lt;/strong&gt; lets you upload your photo and instantly preview multiple hairstyles, complete with AI-generated ratings to help you find your perfect match.&lt;/p&gt;

&lt;p&gt;This app solves a very real problem: most of us take a risk when we try a new hairstyle. By using multimodal AI, you can confidently test different styles virtually before visiting the barber.&lt;/p&gt;

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

&lt;p&gt;🔗 &lt;a href="https://hairstyle-ai-try-on-441360358721.us-west1.run.app" rel="noopener noreferrer"&gt;Live Demo Link&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Screenshots
&lt;/h3&gt;

&lt;p&gt;Initial State (upload your photo):&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%2F6xj1q5kg94ytune9h0rt.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%2F6xj1q5kg94ytune9h0rt.png" alt="Initial"&gt;&lt;/a&gt;&lt;/p&gt;

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

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

&lt;p&gt;AI-Generated Hairstyles:&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%2Ftomcm9iv75bal1muopy1.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%2Ftomcm9iv75bal1muopy1.png" alt="Hairstyle 1"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;h2&gt;
  
  
  How I Used Google AI Studio
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;I built and deployed the entire experience on &lt;strong&gt;Google AI Studio&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The backend runs on &lt;strong&gt;Cloud Run&lt;/strong&gt;, making it scalable and easy to deploy.&lt;/li&gt;
&lt;li&gt;Gemini was used for &lt;strong&gt;image understanding&lt;/strong&gt; (detecting the face and aligning hairstyles) and &lt;strong&gt;content generation&lt;/strong&gt; (evaluating &amp;amp; scoring hairstyles).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Multimodal Features
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Image Understanding&lt;/strong&gt;: The uploaded photo is analyzed to detect face features and properly overlay hairstyles.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image Generation/Transformation&lt;/strong&gt;: Hairstyles are applied virtually, with realistic blending to match lighting and head shape.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Text + Scoring Output&lt;/strong&gt;: Gemini provides natural-language feedback and a numerical “style score” (e.g. 8.5/10 for Curly Fringe).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This mix of visual + evaluative output makes the experience both fun and practical.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Delightful user experience&lt;/strong&gt;: Try on hairstyles virtually, no risk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Practical application&lt;/strong&gt;: Helps people make confident styling decisions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shows multimodal power&lt;/strong&gt;: Combines &lt;strong&gt;vision, generation, and evaluation&lt;/strong&gt; in one workflow.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Team
&lt;/h2&gt;

&lt;p&gt;Solo submission by &lt;a class="mentioned-user" href="https://dev.to/devrayat000"&gt;@devrayat000&lt;/a&gt; &lt;/p&gt;

&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;This project demonstrates how Gemini’s multimodal capabilities can power consumer-friendly, real-world experiences. It’s fun, engaging, and practical — all in one.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>googleaichallenge</category>
      <category>ai</category>
      <category>gemini</category>
    </item>
    <item>
      <title>Highlighting Image Text</title>
      <dc:creator>Zul Ikram Musaddik Rayat</dc:creator>
      <pubDate>Tue, 30 Apr 2024 22:10:00 +0000</pubDate>
      <link>https://dev.to/devrayat000/highlighting-image-text-4hhh</link>
      <guid>https://dev.to/devrayat000/highlighting-image-text-4hhh</guid>
      <description>&lt;p&gt;Image processing and data extraction has become one of the most powerful features of Machine Learning now. But doing it from scratch is a pain in the a**. The one thing programming taught me that no one else did is not to reinvent the wheel every time and to prioritize getting the job done. Keeping that in mind, I have come across an easy solution for the problem at hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem At Hand
&lt;/h2&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%2Fp6fvubs2ftao8gfhjile.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%2Fp6fvubs2ftao8gfhjile.png" alt="Raw image before highlighting" width="800" height="489"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For simplicity's sake, let us consider this to be a page from a book. We want to highlight the word &lt;em&gt;comment&lt;/em&gt; wherever it occurs. This could be an intuitive feature for image search engines to direct the users' attention to their desired content.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution
&lt;/h2&gt;

&lt;p&gt;We are going to be using an OCR (Optical Character Recognition) engine called &lt;a href="https://tesseract-ocr.github.io" rel="noopener noreferrer"&gt;Tesseract&lt;/a&gt; for the image-to-text recognition part. It is free software, released under the Apache License. Install the engine for your desired OS from their official website. I'm using Windows for this. Add the installation path to your environment variables.&lt;/p&gt;

&lt;p&gt;Create a python project with a virtual environment set up on it. Install the necessary packages.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;opencv-python &lt;span class="c"&gt;# for image processing&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;pytesseract &lt;span class="c"&gt;# to use the ocr engine in your project&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;pandas &lt;span class="c"&gt;# to conduct search queries&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In your main.py import the necessary libraries and define the necessary variables. Read the image from the source using the &lt;em&gt;imread&lt;/em&gt; method. Make a copy of the original image for the overlay. Extract text information from the image. It is important to set the &lt;em&gt;output_type&lt;/em&gt; to be a pandas Dataframe object which will ease the filtering process.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;cv2&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pytesseract&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytesseract&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Output&lt;/span&gt;

&lt;span class="n"&gt;ALPHA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.4&lt;/span&gt;

&lt;span class="n"&gt;filename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;devto.png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;comment&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;img&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cv2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;imread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# make a copy of the original image for the highlight overlay
&lt;/span&gt;&lt;span class="n"&gt;overlay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# extract text data from the image as a pandas Dataframe object
&lt;/span&gt;&lt;span class="n"&gt;boxes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pytesseract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;image_to_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lang&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ben+eng&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DATAFRAME&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The dataframe object returned has the following structure:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;level&lt;/th&gt;
&lt;th&gt;page_num&lt;/th&gt;
&lt;th&gt;block_num&lt;/th&gt;
&lt;th&gt;par_num&lt;/th&gt;
&lt;th&gt;line_num&lt;/th&gt;
&lt;th&gt;word_num&lt;/th&gt;
&lt;th&gt;left&lt;/th&gt;
&lt;th&gt;top&lt;/th&gt;
&lt;th&gt;width&lt;/th&gt;
&lt;th&gt;height&lt;/th&gt;
&lt;th&gt;conf&lt;/th&gt;
&lt;th&gt;text&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;169&lt;/td&gt;
&lt;td&gt;537&lt;/td&gt;
&lt;td&gt;99&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;96.276794&lt;/td&gt;
&lt;td&gt;comments&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;We are only interested in the &lt;em&gt;text&lt;/em&gt;, &lt;em&gt;left&lt;/em&gt;, &lt;em&gt;top&lt;/em&gt;, &lt;em&gt;width&lt;/em&gt;, and &lt;em&gt;height&lt;/em&gt; columns. We need to prepare the dataframe for this specific job by applying various filters. Drop the rows that have &lt;em&gt;NaN&lt;/em&gt; or empty string in the &lt;em&gt;text&lt;/em&gt; column to make our data error-proof and the computations more efficient. The text column usually contains single words. We can iterate through each row to find out if any of them matches our query string.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# drop rows that have NaN values in the text column
&lt;/span&gt;&lt;span class="n"&gt;boxes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boxes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dropna&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="c1"&gt;# remove empty text rows
&lt;/span&gt;&lt;span class="n"&gt;boxes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boxes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;boxes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="c1"&gt;# Search through the text column for matching words
&lt;/span&gt;&lt;span class="n"&gt;boxes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;boxes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&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="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;case&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we can get started with the highlighting part. We will draw rectangular highlight boxes around the matched positions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;box&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;boxes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;iterrows&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;left&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;box&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;left&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;top&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;box&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;top&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;box&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;width&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;box&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;height&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="c1"&gt;# draw a yellow rectangle around the matched text
&lt;/span&gt;    &lt;span class="n"&gt;cv2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rectangle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;overlay&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;left&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;left&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;),&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="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Add the overlay on the original image
&lt;/span&gt;&lt;span class="n"&gt;img_new&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cv2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addWeighted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;overlay&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ALPHA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;ALPHA&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="c1"&gt;# Some more image processing to make the highlights more realistic
&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1000.0&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;img_new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;dim&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;img_new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&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="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;resized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cv2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;img_new&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;interpolation&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cv2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;INTER_AREA&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Show the modified image using opencv's &lt;em&gt;imshow&lt;/em&gt; method.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;cv2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;imshow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Highlighted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resized&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;cv2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitKey&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="n"&gt;cv2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;destroyAllWindows&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result is this modified image with every occurring &lt;em&gt;comment&lt;/em&gt; highlighted in yellow.&lt;br&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%2Feaftpbrka1vur4y8l8rm.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feaftpbrka1vur4y8l8rm.jpg" alt="Highlighted image" width="800" height="489"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Bonus Tip
&lt;/h2&gt;

&lt;p&gt;The search-through mechanism in this process can only detect and highlight a single word or full sentence with exact matches. If we want to highlight words that are not in a single sentence, we just need to filter the dataframe with a little bit of pandas magic.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;concat&lt;/span&gt;

&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;boxes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;boxes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&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="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;case&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;boxes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="n"&gt;boxes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;boxes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;word&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;case&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;word&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&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;With this, the user can query "essential comments" and it will highlight &lt;em&gt;essential&lt;/em&gt; and &lt;em&gt;comments&lt;/em&gt; even though they are not together.&lt;/p&gt;

</description>
      <category>python</category>
      <category>opencv</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Hash routing in Remix!</title>
      <dc:creator>Zul Ikram Musaddik Rayat</dc:creator>
      <pubDate>Fri, 29 Jul 2022 15:09:58 +0000</pubDate>
      <link>https://dev.to/devrayat000/hash-routing-in-remix-e2p</link>
      <guid>https://dev.to/devrayat000/hash-routing-in-remix-e2p</guid>
      <description>&lt;p&gt;&lt;a href="https://remix.run/" rel="noopener noreferrer"&gt;Remix&lt;/a&gt; is the newest hottest full-stack React framework. Remix supports file-based routing which uses &lt;code&gt;react-router-dom&lt;/code&gt; under the hood which is a popular react routing library.&lt;/p&gt;

&lt;p&gt;Because Remix users react-router's &lt;em&gt;BrowserRouter&lt;/em&gt; internally, you can't actually do &lt;strong&gt;Hash Routing&lt;/strong&gt; (id based routing that triggers scrolling) with it. &lt;/p&gt;

&lt;p&gt;For example, if you create a link like this,&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Link&lt;/span&gt; &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"#footer"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    Go to bottom
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Link&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;it will surely change the browser URL, but won't scroll down to the element with an id of footer.&lt;/p&gt;

&lt;p&gt;Of course, there's an easier way to achieve this behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turning off Client-Side Routing
&lt;/h2&gt;

&lt;p&gt;We can add a &lt;code&gt;reloadDocument&lt;/code&gt; prop to the specialized &lt;strong&gt;Link&lt;/strong&gt; component and it will start acting like a normal anchor tag.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Link&lt;/span&gt; &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"#footer"&lt;/span&gt; &lt;span class="na"&gt;reloadDocument&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    Go to bottom
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Link&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This in turns, turns off client-side routing and lets the browser handle the transition, as &lt;a href="https://reactrouter.com/docs/en/v6/components/link" rel="noopener noreferrer"&gt;mentioned here&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is fine until you want both client-side routing and scroll behavior on hash routing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Manual Scrolling via Side-Effect
&lt;/h2&gt;

&lt;p&gt;The workaround this is to catch the side-effect created by the route change inside a &lt;code&gt;useEffect&lt;/code&gt; and handle the scrolling manually.&lt;/p&gt;

&lt;p&gt;In the root (&lt;em&gt;root.jsx&lt;/em&gt; file) of our projects, we have to get the &lt;em&gt;location&lt;/em&gt; object from the &lt;code&gt;useLocation&lt;/code&gt; hook provided by remix. Then, we have to create a &lt;em&gt;useEffect&lt;/em&gt; which depends on that location object. The &lt;em&gt;location&lt;/em&gt; object has a property called &lt;em&gt;hash&lt;/em&gt; which provides us with the hash portion of the URL e.g. &lt;strong&gt;#footer&lt;/strong&gt; if the URL is &lt;strong&gt;&lt;a href="http://www.example.com/#footer" rel="noopener noreferrer"&gt;www.example.com/#footer&lt;/a&gt;&lt;/strong&gt;. Then we can look up the element containing that id and manually scroll down to it using the &lt;code&gt;scrollIntoView&lt;/code&gt; method.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;location&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useLocation&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hash&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;el&lt;/span&gt; &lt;span class="o"&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;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hash&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;el&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scrollIntoView&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="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>react</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
