<?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: GeekyAnts</title>
    <description>The latest articles on DEV Community by GeekyAnts (geekyants).</description>
    <link>https://dev.to/geekyants</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%2Forganization%2Fprofile_image%2F12611%2F12ba258b-c1e8-4802-9a86-9447eacb706a.png</url>
      <title>DEV Community: GeekyAnts</title>
      <link>https://dev.to/geekyants</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/geekyants"/>
    <language>en</language>
    <item>
      <title>Building a Real-Time Photo Sync Video Experience Using 100ms SDK and Reactive State Management</title>
      <dc:creator>GeekyAnts India Pvt Ltd</dc:creator>
      <pubDate>Fri, 23 Oct 2026 10:08:14 +0000</pubDate>
      <link>https://dev.to/geekyants/building-a-real-time-photo-sync-video-experience-using-100ms-sdk-and-reactive-state-management-4lf4</link>
      <guid>https://dev.to/geekyants/building-a-real-time-photo-sync-video-experience-using-100ms-sdk-and-reactive-state-management-4lf4</guid>
      <description>&lt;p&gt;This blog explains how to build a real-time video experience with synchronized photo sharing, chat, and reactive state management using the 100ms SDK.&lt;/p&gt;

&lt;p&gt;Building &lt;a href="https://geekyants.com/blog/designing-a-real-time-ai-pipeline-for-human-like-video-conversations" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;u&gt;real-time video experiences&lt;/u&gt;&lt;/strong&gt;&lt;/a&gt; traditionally required deep expertise in WebRTC, signaling infrastructure, media synchronization, and browser-specific handling. With modern SDKs like 100ms, teams can now focus more on product experience and less on low-level media infrastructure.&lt;/p&gt;

&lt;p&gt;In this article, we will walk through how to build a real-time video calling experience with synchronized photo sharing and chat, powered by the 100ms JavaScript SDK, a reactive state architecture, and a &lt;a href="https://geekyants.com/engineering/web-engineering-services" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;u&gt;modular frontend design&lt;/u&gt;&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We will cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Core concepts of 100ms architecture&lt;/li&gt;
&lt;li&gt;Pre-join (green room) experience&lt;/li&gt;
&lt;li&gt;Joining and managing a live video room&lt;/li&gt;
&lt;li&gt;Real-time photo synchronization across participants&lt;/li&gt;
&lt;li&gt;Chat integration using broadcast messaging&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://geekyants.com/blog/react-query-as-a-state-manager-in-nextjs-do-you-still-need-redux-or-zustand" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;u&gt;State management&lt;/u&gt;&lt;/strong&gt;&lt;/a&gt; strategies for responsive UI&lt;/li&gt;
&lt;li&gt;Synchronization correctness, conflict resolution, and reconnect handling&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;High-Level Architecture&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;Before diving into the implementation, it’s worth understanding how the system components interact.&lt;/p&gt;

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

&lt;p&gt;On the backend, when a meetup is created, the server calls the 100ms Rooms API to provision a room and generate authentication tokens for invited participants. Each participant receives a token tied to the same room ID, ensuring everyone joins the same session.&lt;/p&gt;

&lt;p&gt;On the frontend, a single HMSReactiveStore instance acts as the centralized source of truth for media state, participants, tracks, and messages. Both the Green Room and the Live Call screens subscribe to slices of this store rather than maintaining disconnected local state.\&lt;br&gt;
The SDK exposes three key primitives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Store&lt;/strong&gt; → Reactive room state&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Actions&lt;/strong&gt; → Methods for operations such as join, mute, or messaging&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Notifications&lt;/strong&gt; → Event listeners for peer updates and room events&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This architecture provides a predictable data flow for real-time applications and keeps media state, participant state, and UI subscriptions centralized. The synchronized-photo feature still requires explicit conflict-resolution and lifecycle handling, which we address below.&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;Why a Pre-Join (Green Room) Matters&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;A pre-join experience is critical for user confidence and system readiness.\&lt;br&gt;
100ms provides a &lt;strong&gt;preview API&lt;/strong&gt; that allows users to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Test camera and microphone&lt;/li&gt;
&lt;li&gt;Set initial mute/unmute states&lt;/li&gt;
&lt;li&gt;Validate network readiness and authentication before joining&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;Key Implementation Flow&lt;/strong&gt;
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Fetch authentication token from backend&lt;/li&gt;
&lt;li&gt;Call preview() with user config&lt;/li&gt;
&lt;li&gt;Attach local video track to UI&lt;/li&gt;
&lt;li&gt;Allow user to toggle mic and camera
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;hmsActions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preview&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
 &lt;span class="na"&gt;authToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;token from backend&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="na"&gt;userName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Alice&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;Once preview is successful:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Local tracks are created&lt;/li&gt;
&lt;li&gt;Permissions are verified&lt;/li&gt;
&lt;li&gt;Settings persist into the join phase&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This reduces friction during room entry.&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;Setting Up the 100ms SDK&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The 100ms JavaScript SDK ships through @100mslive/hms-video-store.\&lt;br&gt;
Everything flows from a single HMSReactiveStore instance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Initialize once — treated as a module-level singleton&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;HMSReactiveStore&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@100mslive/hms-video-store&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;hmsManager&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;HMSReactiveStore&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;hmsManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;triggerOnSubscribe&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// fire callbacks immediately with current state&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hmsStore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hmsManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getStore&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;hmsActions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hmsManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getActions&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;hmsNotifications&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hmsManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getNotifications&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Calling triggerOnSubscribe() is important because it ensures subscribers immediately receive the current SDK state instead of waiting for the next state update.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Reading State with Selectors&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The store exposes state through memoized selectors rather than raw state objects. This enables efficient reactive subscriptions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;selectPeers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;selectIsLocalVideoEnabled&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;selectIsLocalAudioEnabled&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;selectLocalVideoTrackID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;selectLocalPeer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;selectTrackByID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

  &lt;span class="nx"&gt;selectPeerByID&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@100mslive/hms-video-store&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// One-time read&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isVideoOn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hmsStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;selectIsLocalVideoEnabled&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Reactive subscription — returns an unsubscribe function&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;unsubscribe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hmsStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// runs whenever local video enabled/disabled&lt;/span&gt;
  &lt;span class="nf"&gt;updateVideoButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;selectIsLocalVideoEnabled&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Subscribing to fine-grained selectors helps avoid unnecessary re-renders. In our implementation, this was useful for participant and track updates because changes could be scoped to the affected selector rather than causing broad UI updates.&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;The Green Room: Pre-Join Experience&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;The Green Room acts as a staging area before users join the room.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Attaching Local Video&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Once preview mode starts, we subscribe to the local video track and attach it to a &amp;lt;video&amp;gt; element.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Wire local video into a &amp;lt;video&amp;gt; element&lt;/span&gt;
&lt;span class="nx"&gt;hmsStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&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;trackId&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;trackId&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;videoElement&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;hmsActions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attachVideo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trackId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;videoElement&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;videoElement&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;videoElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;srcObject&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;selectLocalVideoTrackID&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 user disables their camera, the video track disappears and the UI reflects the change.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Toggling Camera and Microphone&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Media controls during preview work the same way as they do during a live call.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Toggle camera&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;hmsStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;selectIsLocalVideoEnabled&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;hmsActions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setLocalVideoEnabled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Toggle mic&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;hmsStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;selectIsLocalAudioEnabled&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;hmsActions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setLocalAudioEnabled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the UI subscribes to the store, button states remain synchronized.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Transitioning to the Live Call&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;When the user clicks "Join", we stop the preview session first by calling hmsActions.leave(). This releases the preview peer from the 100ms session and prevents a ghost local peer from remaining alongside the new peer after the user joins. After leave completes, we navigate to the Live Call route.&lt;/p&gt;

&lt;p&gt;The camera/microphone preferences chosen in the Green Room are persisted to a Pinia store (with localStorage persistence) so the Live Call can restore them on mount without asking the user again.\&lt;br&gt;
To join a room, 100ms requires:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Auth token&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Username&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The join flow is straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;hmsActions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;authToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;savedToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;userName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Alice&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;// Restore preferences from Green Room&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="nx"&gt;hmsActions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setLocalVideoEnabled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;preferredCameraOn&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nx"&gt;hmsActions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setLocalAudioEnabled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;preferredMicOn&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;After joining:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The SDK emits events such as ON_JOIN, ON_PEER_UPDATE, and ON_TRACK_UPDATE&lt;/li&gt;
&lt;li&gt;Reactive store subscriptions keep the UI aligned with the resulting SDK state&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Rendering the Peer Strip&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;All connected peers are available via the selectPeers selector. We subscribe to it and re-render the participant strip whenever anyone joins or leaves. The local peer is sorted to the front; remote peers are sorted by join time.&lt;/p&gt;

&lt;p&gt;Each peer tile has a &amp;lt;video&amp;gt; element whose DOM reference is stored in a Map keyed by peer ID. When a peer's video track changes (e.g. they turn their camera on), we call attachVideo to bind the new track. When a peer leaves, we call detachVideo to clean up.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Subscribe to the peer list — fires on every join/leave&lt;/span&gt;
&lt;span class="nx"&gt;hmsStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;peers&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="nf"&gt;renderParticipants&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;peers&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;selectPeers&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Attach video for a specific peer&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;attachPeerVideo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;peer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;videoEl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;peer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;videoTrack&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;hmsActions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attachVideo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;peer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;videoTrack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;videoEl&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 provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;UI updates when participants join or leave&lt;/li&gt;
&lt;li&gt;Consistent track state across components&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The store acts as a single source of truth for SDK-backed room state, while application-specific state such as the current synchronized photo is handled separately with explicit synchronization rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Showing Live Audio and Video Status&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;For remote peers, track enabled/disabled state is read from the store using selectTrackByID. For the local peer, we use selectIsLocalVideoEnabled and selectIsLocalAudioEnabled directly. This allows the mic icon next to each participant tile to update in real time as participants toggle their devices, without polling.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;hmsActions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attachVideo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trackId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;videoElement&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Important considerations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Handle track changes dynamically&lt;/li&gt;
&lt;li&gt;Detach video when track is removed&lt;/li&gt;
&lt;li&gt;Retry attaching in case of async delays&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Media toggling is handled via actions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;hmsActions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setLocalVideoEnabled&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;await&lt;/span&gt; &lt;span class="nx"&gt;hmsActions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setLocalAudioEnabled&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;State is synced using selectors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;selectIsLocalVideoEnabled&lt;/li&gt;
&lt;li&gt;selectIsLocalAudioEnabled&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Persisting Preferences&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Camera and microphone preferences are stored locally and reused:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;During preview&lt;/li&gt;
&lt;li&gt;During join&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This creates a consistent user experience across sessions.&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;Real-Time Chat via Broadcast Messages&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;Rather than spinning up a separate WebSocket or a third-party chat service, we used 100ms's built-in messaging primitives. hmsActions.sendBroadcastMessage(text) delivers a message to every peer in the room. The SDK stores all messages in HMSStore and exposes them via the selectHMSMessages selector.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Send a chat message&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;hmsActions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendBroadcastMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userInput&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// React to new messages&lt;/span&gt;
&lt;span class="nx"&gt;hmsStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Filter to 'chat' type only — other types are used internally (e.g. photo sync)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chatMessages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;chat&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;renderMessages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chatMessages&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;selectHMSMessages&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One important design decision: messages are typed. The default type for a regular chat message is 'chat'. We use a custom type 'PHOTO_SYNC' for the carousel sync messages described in the next section. The chat panel filters to type === 'chat' so photo-sync events never appear as chat bubbles.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Unread Message Badge&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;We track how many chat messages were visible the last time the panel was open (lastReadCount). When a new message arrives while the panel is closed and the sender isn't the local peer, we set an unread flag that drives a red dot on the chat button. When the panel opens, we reset the count.&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;Synced Image Carousel Across All Peers&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;The synchronized carousel required a separate state-management approach. We wanted participants to converge on the same photo when someone navigated the carousel. We built the synchronization on top of 100ms's broadcast message API, with no additional signaling infrastructure.&lt;/p&gt;

&lt;p&gt;An important distinction is that these messages are navigation events, not durable shared state. Broadcast messaging works for transient actions such as ‘move to photo 5,’ but it should not serve as the canonical state for late joiners. If the application needs the current photo to survive missed messages or reconnects, the 100ms Session Store is a better fit for a canonical session-level state.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Defining the Sync Message Shape&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;We define a typed interface for the payload so that both the sender and all receivers agree on the structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;PhotoSyncMessage&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;navigate_photo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;photoId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Primary resolution: unique ID of the photo&lt;/span&gt;
  &lt;span class="nl"&gt;navigatorId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// which peer triggered the change&lt;/span&gt;
  &lt;span class="nl"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Diagnostic metadata (clocks may drift, do not use for ordering)&lt;/span&gt;
  &lt;span class="nl"&gt;sequenceId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Monotonic counter to reject stale messages&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;strong&gt;Note on Synchronization Correctness&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;To prevent concurrent navigation conflicts (e.g., two users changing photos simultaneously) and distributed clock drift issues, this architecture enforces an authoritative navigator model. The presenter acts as the authoritative navigator. Navigation initiated by other participants is ignored by receivers, preventing simultaneous navigation from producing conflicting shared state. Each accepted navigation event also carries a monotonic sequence ID, allowing receivers to reject stale events.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Sending a Navigation Event&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Whenever the local user taps a thumbnail or presses a carousel arrow, we broadcast a PHOTO_SYNC message alongside updating local state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;AUTHORITATIVE_ROLE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;presenter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// Module-level sequence tracker &lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;localSequenceId&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handlePhotoSelected&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;photo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Only the authoritative navigator can initiate synced navigation &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;currentUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roleName&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;AUTHORITATIVE_ROLE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;localSequenceId&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setCurrentPhotoId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;photo&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="c1"&gt;// local update&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;hmsActions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendBroadcastMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;navigate_photo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;photoId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="nx"&gt;photo&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="na"&gt;navigatorId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;currentUser&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="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="na"&gt;sequenceId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="nx"&gt;localSequenceId&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PHOTO_SYNC&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;         &lt;span class="c1"&gt;// custom type keeps it separate from chat&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;hmsActions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sessionStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;canonical_photo_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;photo&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="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Failed to update canonical session state&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;strong&gt;Receiving and Applying Navigation Events&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;On the receiver side, we hook into hmsNotifications.onNotification(), filter to NEW_MESSAGE events with our custom type, parse and validate the payload, and resolve the target photo by its stable ID before updating local state.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Track the latest applied sync sequence for the room&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;lastSyncSequence&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;// Define which role is allowed to navigate to prevent simultaneous conflicts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;AUTHORITATIVE_ROLE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;presenter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 

&lt;span class="nx"&gt;hmsNotifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onNotification&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;notification&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;notification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;NEW_MESSAGE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;payload&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="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; 
  &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;navigate_photo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// --- 1. AUTHORITATIVE NAVIGATOR CHECK ---&lt;/span&gt;

&lt;span class="c1"&gt;// Restrict synchronization to an authoritative peer role.&lt;/span&gt;
&lt;span class="c1"&gt;// This prevents multiple participants from independently&lt;/span&gt;
&lt;span class="c1"&gt;// updating the shared navigation state.&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;senderPeer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hmsStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;selectPeerByID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;navigatorId&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;senderPeer&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;roleName&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;AUTHORITATIVE_ROLE&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;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Rejected navigation event from non-authoritative peer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// --- 2. DETERMINISTIC SEQUENCE ORDERING ---&lt;/span&gt;
  &lt;span class="c1"&gt;// Reject out-of-order/stale messages from the authoritative peer&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;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sequenceId&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;lastSyncSequence&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;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Rejected stale navigation sequence&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// --- 3. STABLE ID RESOLUTION (No Index Fallback) ---&lt;/span&gt;
  &lt;span class="c1"&gt;// Match strictly by stable photoId. If lists diverge and ID is unknown, &lt;/span&gt;
  &lt;span class="c1"&gt;// do not guess using index. Ignore event and rely on Session Store recovery.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;targetPhoto&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;photos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&amp;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="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;photoId&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;targetPhoto&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;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Photo ID not found in local state, ignoring event.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Apply state and update tracker&lt;/span&gt;
  &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setCurrentPhotoId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;targetPhoto&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="nx"&gt;lastSyncSequence&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sequenceId&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;
  
  
  &lt;strong&gt;Syncing New Joiners&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;A participant who joins mid-session could see the carousel start at index 0 while the rest of the room has moved to another photo. For late joiners, the implementation relies on the 100ms Session Store instead of allowing multiple peers to broadcast competing states. When a user joins, they simply read canonical_photo_id from the Session Store to land on the correct image.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Persisting Media Preferences Between Screens&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The user selects their camera and mic state in the Green Room. These preferences need to persist through the route change to the Live Call screen and a page refresh.&lt;/p&gt;

&lt;p&gt;We store preferredCameraOn and preferredMicOn in a Pinia store configured with selective localStorage persistence. Only these two fields are persisted; sensitive data like the HMS auth token is kept in memory only and is re-fetched on every session.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Pinia store — persist only media prefs&lt;/span&gt;
&lt;span class="nx"&gt;persist&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;app-meetup&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;paths&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;preferredCameraOn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;preferredMicOn&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;When the Live Call screen mount, we read these two values back and call setLocalVideoEnabled / setLocalAudioEnabled immediately after join() resolves. This means the peer publishes with the camera and microphone state configured in the Green Room.&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;Live Session Teardown&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;Proper session teardown prevents stale participant and media state.&lt;/p&gt;

&lt;p&gt;If a participant disconnects without completing session teardown, other users may see stale or ‘ghost’ participants for a short period.&lt;/p&gt;

&lt;p&gt;We handle cleanup in three places:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Explicit leave button&lt;/li&gt;
&lt;li&gt;Browser unload events&lt;/li&gt;
&lt;li&gt;Component unmount lifecycle hooks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For browser lifecycle handling, do not rely on unload alone. Modern browsers may skip or limit unload processing, so lifecycle handling should account for pagehide, beforeunload, and freeze where appropriate. These events should be treated as best-effort lifecycle signals rather than a guarantee that an awaited asynchronous leave() operation will finish before the page disappears.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;explicitTeardown&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;unsubscribers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;unsub&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;unsub&lt;/span&gt;&lt;span class="p"&gt;?.());&lt;/span&gt;
  &lt;span class="c1"&gt;// For explicit leave button, we safely await&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;hmsActions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;leave&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// For browser lifecycle (closing tab, backgrounding, freezing)&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;setupBrowserLifecycleTeardown&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;bestEffortCleanup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;unsubscribers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;unsub&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;unsub&lt;/span&gt;&lt;span class="p"&gt;?.());&lt;/span&gt;
    &lt;span class="c1"&gt;// Do not await leave() during lifecycle events; it's a best-effort fire-and-forget&lt;/span&gt;
    &lt;span class="nx"&gt;hmsActions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;leave&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;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="c1"&gt;// Modern alternatives to the deprecated 'unload' event&lt;/span&gt;
  &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pagehide&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bestEffortCleanup&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;beforeunload&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bestEffortCleanup&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;freeze&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bestEffortCleanup&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;For an explicit leave action or route transition, the async leave() call can be awaited normally. For browser termination events, cleanup should be best-effort and should not assume asynchronous work will always complete. The application should also remove subscriptions and detach UI references during component teardown, while relying on the SDK/browser lifecycle for connection termination.&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;Performance and UX Considerations&lt;/strong&gt;
&lt;/h1&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;1. Avoid Duplicate State&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Use the reactive store instead of local component state wherever possible.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;2. Lazy Rendering&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Attach video tracks only when elements are mounted.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;3. Optimistic UI Updates&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Update the UI when the user changes a photo, then synchronize the change.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;4. Graceful Error Handling&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Token failures&lt;/li&gt;
&lt;li&gt;Permission issues&lt;/li&gt;
&lt;li&gt;Network interruptions&lt;/li&gt;
&lt;li&gt;Reconnects and late joiners&lt;/li&gt;
&lt;li&gt;Unknown or stale PHOTO_SYNC messages&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For synchronized navigation, network interruption is especially important: a participant may miss transient broadcast messages. The design should recover from canonical session-level state when the product requires convergence after reconnect.&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;Synchronization Validation and Failure Cases&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;Before describing a real-time synchronization flow as production-ready, it is important to validate failure modes rather than relying only on the happy path. Useful acceptance checks include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rapid navigation:&lt;/strong&gt; repeatedly move forward/backward and verify that older messages do not overwrite the latest accepted sequence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simultaneous navigation:&lt;/strong&gt; have two participants navigate at the same time and verify that the defined authority/conflict-resolution rule produces one deterministic final state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Late join:&lt;/strong&gt; join after the room has already moved through several photos and verify that the participant converges to the canonical current photo rather than starting at index 0.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reconnect:&lt;/strong&gt; interrupt a participant's network connection, restore it, and verify that the participant recovers the current session state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Divergent photo ordering:&lt;/strong&gt; use stable photoId matching rather than assuming every participant's array has identical ordering.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Browser lifecycle:&lt;/strong&gt; leave through the explicit button, route away, background/freeze the page, and close the tab to verify that subscriptions and media references are cleaned up as far as the browser lifecycle permits.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These checks make the synchronization guarantees explicit. When behavior has not been measured under a defined participant count or network condition, describe the implementation as a maintainable foundation rather than making an absolute scalability or production-readiness claim.&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;Extensibility Opportunities&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;This architecture can support additional capabilities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Screen sharing&lt;/li&gt;
&lt;li&gt;Role-based permissions&lt;/li&gt;
&lt;li&gt;&lt;a href="https://geekyants.com/blog/building-production-grade-video-thumbnail-scrubbing-in-the-browser-hls-frame-extraction-caching-and-performance-trade-offs" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;u&gt;Recording and streaming&lt;/u&gt;&lt;/strong&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Collaborative annotations&lt;/li&gt;
&lt;li&gt;Session-level shared state using the Session Store&lt;/li&gt;
&lt;li&gt;Deterministic conflict resolution for collaborative controls&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;Key Takeaways&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;Building this feature reinforced several architectural patterns for real-time applications:&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;1. Centralized reactive state simplifies synchronization&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Maintaining a single HMSReactiveStore instance prevents UI desynchronization and keeps media state consistent across screens.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;2. Preview mode improves pre-join UX&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The Green Room gives users confidence and control before entering the live session.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;3. Broadcast messaging can support multiple event types&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;We used the same messaging layer for chat and photo-navigation events by namespacing message types. For durable shared state, such as the current photo for late joiners, session-level state is a better model.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;4. Fine-grained subscriptions improve performance&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Track-level subscriptions prevent unnecessary participant list re-renders.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;5. Clean teardown logic matters&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Explicit transitions should await leave(), while browser lifecycle cleanup must be treated as best-effort and designed around modern lifecycle events rather than assuming unload will complete asynchronous work.&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;Conclusion&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;Real-time collaboration is no longer limited to basic video calls.&lt;/p&gt;

&lt;p&gt;Combining 100ms’s reactive SDK architecture with a structured frontend state model supports interactive shared experiences without requiring a separate synchronization layer for every feature.&lt;/p&gt;

&lt;p&gt;The synchronized-media feature also highlights an important engineering lesson: real-time UX requires more than broadcasting an event. Correctness depends on defining the source of truth, resolving concurrent updates, rejecting stale messages, handling late joiners and reconnects, and managing cleanup across browser lifecycle states.&lt;/p&gt;

&lt;p&gt;With those considerations in place, the patterns described here provide a maintainable foundation for collaborative experiences involving shared media, live messaging, and reactive participant management.\&lt;br&gt;
As real-time applications expand beyond traditional conferencing, reactive synchronization, centralized media state, and event-driven collaboration provide a foundation for shared digital experiences.&lt;/p&gt;

&lt;p&gt;For teams building real-time video and interactive media products, GeekyAnts provides &lt;a href="https://geekyants.com/industry-expertise/media-entertainment" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;u&gt;Media &amp;amp; Entertainment software development services&lt;/u&gt;&lt;/strong&gt;&lt;/a&gt; for developing and scaling digital media experiences.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>programming</category>
      <category>devops</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Stop Automating Everything: A Balanced Quality Engineering Approach to Testing</title>
      <dc:creator>GeekyAnts India Pvt Ltd</dc:creator>
      <pubDate>Mon, 12 Oct 2026 08:55:20 +0000</pubDate>
      <link>https://dev.to/geekyants/stop-automating-everything-a-balanced-quality-engineering-approach-to-testing-1h76</link>
      <guid>https://dev.to/geekyants/stop-automating-everything-a-balanced-quality-engineering-approach-to-testing-1h76</guid>
      <description>&lt;p&gt;Balanced quality engineering places automation, API testing, exploratory work, and AI where each gives the most value, so teams ship faster without trading away user-perceived quality.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Automate by Risk&lt;/strong&gt;: Focus automation on &lt;strong&gt;business-critical, high-risk workflows&lt;/strong&gt; rather than raw test-case count, since more tests do not mean better quality.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cheapest Effective Layer&lt;/strong&gt;: Validate business logic through &lt;a href="https://geekyants.com/engineering/quality-assurance/api-testing" rel="noopener noreferrer"&gt;&lt;strong&gt;API tests&lt;/strong&gt;&lt;/a&gt; where possible and reserve &lt;strong&gt;UI automation&lt;/strong&gt; for the user journeys that matter most.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Keep Exploratory Testing&lt;/strong&gt;: Human testers surface &lt;strong&gt;unexpected behaviour&lt;/strong&gt;, &lt;strong&gt;usability gaps&lt;/strong&gt;, and &lt;strong&gt;edge cases&lt;/strong&gt; that predefined tests never anticipate.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Measure Quality, Not Volume&lt;/strong&gt;: Track &lt;strong&gt;defect detection&lt;/strong&gt;, &lt;strong&gt;leakage&lt;/strong&gt;, &lt;strong&gt;flakiness&lt;/strong&gt;, &lt;strong&gt;maintenance effort&lt;/strong&gt;, and &lt;strong&gt;investigation time&lt;/strong&gt; instead of counting automated tests.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://geekyants.com/ai-accelerator" rel="noopener noreferrer"&gt;&lt;strong&gt;AI as an Accelerator&lt;/strong&gt;&lt;/a&gt;: Let AI assist with &lt;strong&gt;test generation&lt;/strong&gt;, &lt;strong&gt;failure analysis&lt;/strong&gt;, and &lt;strong&gt;maintenance&lt;/strong&gt; while engineers stay responsible for &lt;strong&gt;quality decisions&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Why Passing QA Tests Can Still Ship Poor Experiences&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Software teams are expected to release faster while maintaining reliable digital experiences. Automation has become a critical part of achieving this goal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A typical automated regression workflow looks like:&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;This provides fast and repeatable feedback, but passing tests do not necessarily mean that users are experiencing a high-quality product.&lt;/p&gt;

&lt;p&gt;For example, an automated checkout test may verify that an order is successfully created while missing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;confusing error messages,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;poor mobile usability,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;inaccessible controls,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;unclear navigation,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;unexpected user workflows.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This creates an important distinction:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Technical correctness and user-perceived quality are related, but they are not the same thing.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Moving From Test Automation to Quality Engineering&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Traditional testing often follows:&lt;/p&gt;

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

&lt;p&gt;A &lt;a href="https://geekyants.com/service/hire-quality-assurance-developers" rel="noopener noreferrer"&gt;&lt;strong&gt;Quality Engineering&lt;/strong&gt;&lt;/a&gt; approach distributes quality throughout the lifecycle:&lt;/p&gt;

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

&lt;p&gt;The goal is not simply to find more bugs. It is to provide fast, reliable quality feedback at the appropriate layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What a Balanced Testing Model Looks Like&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;A balanced QE strategy can combine five capabilities:&lt;/p&gt;

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

&lt;p&gt;Each layer has a different purpose.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;colgroup&gt;
&lt;col&gt;
&lt;col&gt;
&lt;/colgroup&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;Testing approach&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;Primary purpose&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Unit testing&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Validate isolated logic&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;API testing&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Validate services and business rules&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;UI automation&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Validate critical user journeys&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Exploratory testing&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Discover unexpected behavior&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;UX testing&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Validate the user experience&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;AI assistance&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Accelerate testing activities&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Deciding What to Automate&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Not every scenario should become a &lt;a href="https://geekyants.com/engineering/quality-assurance/automation-testing-services" rel="noopener noreferrer"&gt;&lt;strong&gt;UI automation test&lt;/strong&gt;&lt;/a&gt;. Strong automation candidates are generally:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;repeatable,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;deterministic,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;frequently executed,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;business-critical,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;expensive to execute manually.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Login&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Checkout&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Payment&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Order creation&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Permission validation&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;API contracts&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A useful decision process is:&lt;/p&gt;

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

&lt;p&gt;The objective should not be 100% automation.&lt;/p&gt;

&lt;p&gt;It should be meaningful risk coverage.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Validate Business Rules Through APIs, Not the UI&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;One common mistake is validating every business rule through the UI.&lt;/p&gt;

&lt;p&gt;Suppose an application exposes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /api/orders
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The business logic can be validated directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const response = await request.post("/api/orders", {
  data: {
    productId: 123,
    quantity: 2
  }
});

expect(response.status()).toBe(201);

const body = await response.json();

expect(body.quantity).toBe(2);
expect(body.status).toBe("created");
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The UI test can then focus on whether a user can successfully complete the workflow. This creates a useful separation:&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;This can reduce unnecessary UI automation and make regression suites faster and easier to maintain.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What Human Testers Catch That Automation Misses&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Automation executes what engineers explicitly define.&lt;/p&gt;

&lt;p&gt;Humans can investigate what was not anticipated.&lt;/p&gt;

&lt;p&gt;For example, an exploratory checkout charter might include:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Explore checkout with focus on:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;invalid inputs&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;browser back button&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;session expiry&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;duplicate submission&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;slow network&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;mobile viewport&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;unexpected navigation&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A tester might discover:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;colgroup&gt;
&lt;col&gt;
&lt;col&gt;
&lt;/colgroup&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;Field&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;Observation&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Action&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Submit payment twice quickly.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Expected&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;One order.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Actual&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Two orders created.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Risk&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Duplicate order/payment.&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A standard happy-path automation test might never discover this scenario. This is why &lt;a href="https://geekyants.com/engineering/quality-assurance/manual-testing-services" rel="noopener noreferrer"&gt;&lt;strong&gt;exploratory testing&lt;/strong&gt;&lt;/a&gt; should not disappear when automation increases.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Why a Green Suite Isn't Proof of a Good Experience&lt;/strong&gt;
&lt;/h2&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;await page.getByRole("button", {
  name: "Place Order"
}).click();

await expect(
  page.getByText("Order confirmed")
).toBeVisible();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test proves that the order can be placed.&lt;/p&gt;

&lt;p&gt;It does not necessarily prove that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;the button is easy to find,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;the error messages are understandable,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;the workflow is accessible,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;the mobile experience is usable,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;the customer understands the payment status.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is where human-centered testing complements automation.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Metrics That Show Whether the Strategy Works&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;A balanced QE strategy should be measurable.&lt;/p&gt;

&lt;p&gt;Instead of measuring only:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Number of automated tests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;track metrics such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Execution time&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Defects detected&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Defect leakage&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Flaky tests&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Maintenance effort&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Failure investigation time&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Exploratory findings&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A practical experiment can use a fixed set of application workflows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Registration&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Login&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Product Search&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Cart&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Checkout&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Payment&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Order Confirmation&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Invoice Download&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then evaluate them using:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;UI Automation&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;API Testing&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Exploratory Testing&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;UX-focused Testing&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The experiment should record the same measures for each testing approach. Until real results are available, it is better to define the measurement plan rather than show placeholder values.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;colgroup&gt;
&lt;col&gt;
&lt;col&gt;
&lt;/colgroup&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;Metric&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;What to record&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Execution time&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Time required to complete the selected workflow or test set&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Unique defects detected&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Distinct defects found by each testing approach&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Maintenance effort&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Time spent updating or repairing tests as the application changes&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Investigation time&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Time required to understand and classify a failed test or observed issue&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Defect leakage&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Defects that escaped earlier test layers and were found later&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Flaky-test rate&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Intermittent automated failures that do not consistently reproduce&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Exploratory findings&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Unexpected behaviors, usability problems, or risk scenarios discovered through human investigation&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Charts Worth Publishing Once You Have Real Data&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Once the experiment has produced real measurements, two graphs can communicate the results clearly without making the article visually heavy:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Execution Time by Testing Approach&lt;/strong&gt; — compare the measured execution time for UI automation, API testing, exploratory testing, and UX-focused testing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Unique Defects Detected by Testing Approach&lt;/strong&gt; — compare the number of distinct defects found by each approach, ideally with defect categories called out where useful.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Do not publish these charts with illustrative or fabricated values. Generate them only from the completed experiment and label the units, sample size, and test conditions so readers can interpret the results correctly.&lt;/p&gt;

&lt;p&gt;These graphs should reinforce an important point:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Different testing approaches detect different categories of risk.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Keeping Automation Reliable Enough to Trust&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Automation becomes less valuable when teams stop trusting test results.&lt;/p&gt;

&lt;p&gt;A test that behaves like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Run 1 → PASS
Run 2 → PASS
Run 3 → FAIL
Run 4 → PASS
Run 5 → PASS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is potentially flaky.&lt;/p&gt;

&lt;p&gt;Avoid solving this by adding arbitrary waits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;await page.waitForTimeout(5000);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead, synchronize with application state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;await expect(
  page.locator('[data-testid="order-confirmation"]')
).toBeVisible();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The principle is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wait for conditions, not time.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Automation should be treated as production-quality software, with code review, maintainability, monitoring, and refactoring.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Where AI Helps — and Where It Shouldn't Decide&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;AI can extend the QE workflow without replacing deterministic testing.&lt;/p&gt;

&lt;p&gt;A practical architecture is:&lt;/p&gt;

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

&lt;p&gt;For example, AI can analyze:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://geekyants.com/blog/how-ai-ml-are-transforming-quality-assurance-in-software-testing-with-playwright-examples" rel="noopener noreferrer"&gt;&lt;strong&gt;Playwright Trace&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Screenshot&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;DOM&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Console Logs&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Network Errors&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Test History&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;and suggest a failure category:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Test defect&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Application defect&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Environment issue&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Data issue&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Network issue&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;However, the engineer should validate the result.&lt;/p&gt;

&lt;p&gt;A useful operating model is:&lt;/p&gt;

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

&lt;p&gt;This keeps AI as an accelerator rather than an uncontrolled decision-maker.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Fitting the Approach Into Your CI/CD Pipeline&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The balanced approach fits naturally into a modern CI/CD pipeline:&lt;/p&gt;

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

&lt;p&gt;Different test layers provide feedback at different speeds, allowing teams to avoid using expensive UI tests for problems that can be detected earlier.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Trade-Offs Between Testing Approaches&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;There is no single testing technique that covers every quality dimension.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;colgroup&gt;
&lt;col&gt;
&lt;col&gt;
&lt;col&gt;
&lt;/colgroup&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;Approach&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;Strength&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;&lt;strong&gt;Limitation&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;UI Automation&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;End-to-end regression&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Maintenance cost&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;API Testing&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Fast business validation&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Doesn't validate UX&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Exploratory&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Finds unexpected issues&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Less repeatable&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;UX Testing&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;User experience&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Requires human effort&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;AI Testing&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Faster analysis/generation&lt;/p&gt;&lt;/td&gt;
&lt;td colspan="1" rowspan="1"&gt;&lt;p&gt;Requires validation&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The right strategy combines these capabilities according to product risk.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;A Reference Architecture You Can Adapt&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The complete model can be summarized as:&lt;/p&gt;

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

&lt;p&gt;This model does not attempt to eliminate human testing.&lt;/p&gt;

&lt;p&gt;Instead, it places automation, APIs, AI, and human reasoning where each provides the most value.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Conclusion: Getting the Balance Right&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Modern Quality Engineering should not be a competition between manual testing and automation.&lt;/p&gt;

&lt;p&gt;Automation provides speed, repeatability, and scalable regression coverage. API testing provides efficient validation of business behavior. Exploratory and human-centered testing provide contextual reasoning and help identify problems that deterministic automation may miss. AI can further accelerate these activities by assisting with generation, analysis, and maintenance.&lt;/p&gt;

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

&lt;p&gt;The most effective Quality Engineering strategy is one that uses technology to accelerate testing without losing sight of the person ultimately using the software.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The goal is not maximum automation. The goal is maximum meaningful quality feedback for the engineering effort invested.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>webdev</category>
      <category>devops</category>
      <category>programming</category>
    </item>
    <item>
      <title>From Rolling Deployments to Zero-Downtime Releases</title>
      <dc:creator>GeekyAnts India Pvt Ltd</dc:creator>
      <pubDate>Wed, 07 Oct 2026 09:17:22 +0000</pubDate>
      <link>https://dev.to/geekyants/from-rolling-deployments-to-zero-downtime-releases-k55</link>
      <guid>https://dev.to/geekyants/from-rolling-deployments-to-zero-downtime-releases-k55</guid>
      <description>&lt;p&gt;Executive Summary&lt;br&gt;
In &lt;a href="https://geekyants.com/industry/fintech-app-development-services" rel="noopener noreferrer"&gt;digital banking&lt;/a&gt;, deployment availability is a business requirement as much as a technical one. A deployment that interrupts access to online banking can affect customers attempting to log in, view balances, initiate transfers, or perform other critical operations.&lt;/p&gt;

&lt;p&gt;Our earlier rolling deployment model introduced problems around version coexistence. Every new frontend build generated build-specific resources such as runtime.1234.js and main.1234.js. During a rolling restart, old and new pods could coexist. A request for a resource from the new build could reach an older pod where that resource did not exist, producing a 404 response.&lt;/p&gt;

&lt;p&gt;Static resources were cached at the CDN for about four minutes. We needed to consider application availability and version consistency across the application shell, static resources, CDN, and backend pods.&lt;/p&gt;

&lt;p&gt;We moved to a &lt;a href="https://geekyants.com/engineering/devops/infrastructure-deployment-automation" rel="noopener noreferrer"&gt;Blue-Green deployment strategy&lt;/a&gt;. The new version is deployed into a separate environment, its pods start and pass readiness checks, and the environment is validated before customer traffic is switched. The existing environment continues serving production traffic until the cutover.&lt;/p&gt;

&lt;p&gt;In our environment, the observed ALB traffic transition from Blue to Green was about 40–180 ms. The key benefit is not faster application startup. Pod creation, image pulling, startup, and readiness occur outside the customer-facing cutover window. The previous environment also remains available so that traffic can be routed back to it if a rollback is required.&lt;/p&gt;

&lt;p&gt;A race condition remained during the ALB transition under high request volumes. A request for a build-specific static resource could reach the previous environment and receive a 404 response. We addressed this with a targeted NGINX retry for the affected static resources, with a delay of about one second before the retry. &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Why We Needed a Different Deployment Strategy
1.1 The Rolling Deployment Model
A rolling deployment replaces application instances in stages. This avoids replacing every instance at once, but it also means different application versions can coexist during the deployment.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;1.2 Build-Specific Static Resources&lt;br&gt;
Every application build generates resources associated with that build. For example:&lt;/p&gt;

&lt;p&gt;Build 100:&lt;br&gt;
runtime.100.js&lt;br&gt;
main.100.js&lt;br&gt;
styles.100.css&lt;/p&gt;

&lt;p&gt;Build 101:&lt;br&gt;
runtime.101.js&lt;br&gt;
main.101.js&lt;br&gt;
styles.101.css&lt;/p&gt;

&lt;p&gt;The browser receives an application shell that references a particular build's resources. If a request for runtime.101.js reaches a Build 100 pod, the resource does not exist there, and the server can return a 404 response.&lt;/p&gt;

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

&lt;p&gt;The pod itself could be healthy. The failure was caused by a version mismatch between the resource requested by the client and the application instance handling the request.&lt;/p&gt;

&lt;p&gt;1.3 CDN Caching Added Another Timing Dimension&lt;br&gt;
Static resources were cached at the CDN for about four minutes. The application origin could move to a new version while cached resources from the previous version remained available at the edge.&lt;/p&gt;

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

&lt;p&gt;Deployment correctness had to account for three independent timing domains: application and pod readiness, ALB traffic convergence, and CDN cache lifetime.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Blue-Green Deployment Architecture
Blue-Green deployment introduced two complete application environments. Blue is the active version, and Green is the new version.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;The principle is simple: deploy and validate Green before allowing it to receive production traffic. This separates deployment preparation from production traffic cutover.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What Happens During a Blue-Green Deployment
3.1 Build and Image Creation
The CI/CD pipeline creates an immutable image for the release. The frontend resources generated by that build are part of the same release artifact.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;This preserves a clear relationship between the application version and the resources it serves.&lt;/p&gt;

&lt;p&gt;3.2 Creating the Green Pods&lt;br&gt;
The new release is deployed to Green without terminating the active Blue environment.&lt;/p&gt;

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

&lt;p&gt;The Green pods go through image pull, container startup, application initialization, and readiness checks before they become eligible to receive traffic.&lt;/p&gt;

&lt;p&gt;3.3 Pod Readiness&lt;br&gt;
Readiness is a key mechanism in this architecture. &lt;a href="https://geekyants.com/engineering/devops/kubernetes-containerization-services" rel="noopener noreferrer"&gt;Kubernetes&lt;/a&gt; distinguishes a running container from a container that is ready to accept traffic. A failed readiness probe causes the pod to be treated as not ready for normal service traffic. A startup probe can be used when an application needs more initialization time before liveness or readiness checks begin.&lt;/p&gt;

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

&lt;p&gt;For a banking application, readiness should represent the minimum state in which the application can process production requests without creating service risk. Depending on the implementation, this can include application initialization, configuration loading, dependency availability, and other required startup conditions.&lt;/p&gt;

&lt;p&gt;3.4 Green Validation&lt;br&gt;
Once the Green pods are Ready, the new environment can be validated before customer traffic is moved.&lt;/p&gt;

&lt;p&gt;Confirm that the pods and application are healthy.&lt;br&gt;
Run API &lt;a href="https://geekyants.com/service/hire-quality-assurance-developers" rel="noopener noreferrer"&gt;smoke tests&lt;/a&gt; against the new environment.&lt;br&gt;
Confirm that the required static resources are available.&lt;br&gt;
Check connectivity to critical dependencies.&lt;br&gt;
Test business-critical flows.&lt;br&gt;
Review application and infrastructure metrics.&lt;br&gt;
This allows the team to validate Green while Blue continues serving production traffic.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;How the ALB Traffic Switch Works
The Application Load Balancer (ALB) provides the boundary for switching production traffic between Blue and Green. An ALB listener evaluates routing rules and forwards traffic to target groups. Target groups contain registered targets and can have their own health checks.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;Before the cutover, Blue is the active target group and Green is prepared without receiving normal production traffic. Once Green has passed the deployment gates, the routing configuration changes so that production requests are directed to Green.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The Critical Difference: Deployment Time vs Customer-Impact Time
A key distinction in Blue-Green deployment is the difference between deployment time and customer-impact time. The new pods still need to pull the image, start the container, initialize the application, and pass readiness checks.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What changes is where that work occurs.&lt;/p&gt;

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

&lt;p&gt;Blue continues serving customers while Green is being prepared. This removes the time-consuming preparation work from the customer-facing cutover path.&lt;/p&gt;

&lt;p&gt;The customer-facing transition is limited to the routing window rather than the full duration required to create and warm the new pods.&lt;/p&gt;

&lt;p&gt;The improvement should not be described as “the deployment became X% faster.” The deployment preparation time still exists, but it does not need to translate into customer downtime.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Rollback: Turning Recovery Into a Traffic Switch
Blue-Green changes the rollback mechanism.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Normal state:&lt;/p&gt;

&lt;p&gt;BLUE  = Build 100&lt;br&gt;
GREEN = Build 101&lt;br&gt;
Traffic = 100% Green&lt;br&gt;
If monitoring identifies a production problem after the cutover, Blue can remain available as the previous known-good version.&lt;/p&gt;

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

&lt;p&gt;Instead of rebuilding and redeploying the previous image, the deployment can route traffic back to the running Blue environment.&lt;/p&gt;

&lt;p&gt;This changes rollback from a deployment operation into a traffic-routing operation, which can reduce mean time to recovery (MTTR).&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The Production Challenge After Blue-Green
7.1 The ALB Transition Window
Blue-Green solved the major version-coexistence problem, but production testing exposed a smaller race condition during the traffic switch.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In our environment, the observed ALB transition from Blue to Green was about 40–180 ms.&lt;/p&gt;

&lt;p&gt;Before:&lt;br&gt;
Blue  = 100%&lt;br&gt;
Green =   0%&lt;/p&gt;

&lt;p&gt;Transition:&lt;br&gt;
Blue  = 100% -&amp;gt; 0%&lt;br&gt;
Green =   0% -&amp;gt; 100%&lt;/p&gt;

&lt;p&gt;After:&lt;br&gt;
Blue  =   0%&lt;br&gt;
Green = 100%&lt;br&gt;
Under high request volume, requests can arrive during this short interval. A request that arrives during the transition can encounter the previous routing state.&lt;/p&gt;

&lt;p&gt;7.2 Static Resource 404 During the Transition&lt;/p&gt;

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

&lt;p&gt;This was not a persistent application failure. It was a transient race condition caused by the interaction between build-specific resource names and traffic convergence.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;NGINX Retry as the Last-Mile Resilience Layer
To address this transient condition, a targeted retry mechanism was introduced at NGINX for the affected static resources.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;The one-second delay was longer than the observed ALB transition window. This gave the traffic transition time to complete before the retry.&lt;/p&gt;

&lt;p&gt;This mechanism was scoped to static-resource requests. A generic retry policy should not be applied to state-changing banking operations such as fund transfers without idempotency and transaction-safety guarantees.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;CDN and Resource Versioning
Build-specific resource names remain part of the design. Instead of overwriting a stable path such as /main.js, each build produces a unique resource path.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;/main.100.js&lt;br&gt;
/main.101.js&lt;br&gt;
/main.102.js&lt;br&gt;
This allows multiple versions of a resource to coexist in the CDN. The application shell should reference the resource set belonging to its build.&lt;/p&gt;

&lt;p&gt;The CDN cache lifetime of about four minutes is treated as a separate concern from the ALB transition. Build-specific resources provide coexistence, while the NGINX retry addresses the short traffic-convergence race.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Zero-Downtime as an End-to-End Property
The implementation showed that zero-downtime (ZDT) is not a property of Kubernetes or the load balancer alone.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Client&lt;br&gt;
  |&lt;br&gt;
CDN&lt;br&gt;
  |&lt;br&gt;
ALB&lt;br&gt;
  |&lt;br&gt;
NGINX&lt;br&gt;
  |&lt;br&gt;
Kubernetes&lt;br&gt;
  |&lt;br&gt;
Pod readiness&lt;br&gt;
  |&lt;br&gt;
Application&lt;br&gt;
  |&lt;br&gt;
Dependencies / data layer&lt;/p&gt;

&lt;p&gt;A system can have healthy pods and still return customer-visible errors if resource versioning, caching, or routing transitions are not handled correctly.&lt;/p&gt;

&lt;p&gt;Our final approach combines several mechanisms:&lt;/p&gt;

&lt;p&gt;Blue-Green deployment&lt;br&gt;
        +&lt;br&gt;
Pod readiness&lt;br&gt;
        +&lt;br&gt;
Build-specific static resources&lt;br&gt;
        +&lt;br&gt;
CDN caching strategy&lt;br&gt;
        +&lt;br&gt;
ALB traffic cutover&lt;br&gt;
        +&lt;br&gt;
NGINX static-resource retry&lt;br&gt;
        +&lt;br&gt;
Fast rollback&lt;br&gt;
        =&lt;br&gt;
Zero-Downtime Release Architecture&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Business and Operational Impact
Before the move toward ZDT, deployments were scheduled during low-traffic periods such as night-time maintenance windows. This was a risk-management decision. If a deployment could cause service interruption, a period with fewer customers using the platform reduced the number of users exposed to that interruption.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Before:&lt;br&gt;
Release Ready&lt;br&gt;
    |&lt;br&gt;
    v&lt;br&gt;
Wait for Night&lt;br&gt;
    |&lt;br&gt;
    v&lt;br&gt;
Maintenance Window&lt;br&gt;
    |&lt;br&gt;
    v&lt;br&gt;
Deployment&lt;/p&gt;

&lt;p&gt;Blue-Green changed the release model.&lt;/p&gt;

&lt;p&gt;After:&lt;br&gt;
Release Ready&lt;br&gt;
    |&lt;br&gt;
    v&lt;br&gt;
Deploy Green&lt;br&gt;
    |&lt;br&gt;
    v&lt;br&gt;
Validate&lt;br&gt;
    |&lt;br&gt;
    v&lt;br&gt;
Switch Traffic&lt;/p&gt;

&lt;p&gt;Because Blue remains available while Green is prepared, a new deployment does not require the application to be taken offline. Subject to change-management, monitoring, and business controls, teams can perform deployments during normal operating hours, including the morning, instead of restricting them to night-time maintenance windows to avoid deployment downtime.&lt;/p&gt;

&lt;p&gt;This changes the operational model: application availability no longer dictates the deployment window.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Deployment Sequence&lt;/li&gt;
&lt;li&gt;Developer merges release
      |&lt;/li&gt;
&lt;li&gt;CI/CD builds application
      |&lt;/li&gt;
&lt;li&gt;Build-specific static resources generated
      |&lt;/li&gt;
&lt;li&gt;Immutable container image created
      |&lt;/li&gt;
&lt;li&gt;Image pushed to registry
      |&lt;/li&gt;
&lt;li&gt;Green deployment created
      |&lt;/li&gt;
&lt;li&gt;Green pods start
      |&lt;/li&gt;
&lt;li&gt;Startup / initialization
      |&lt;/li&gt;
&lt;li&gt;Readiness probes pass
      |&lt;/li&gt;
&lt;li&gt;Green validation / smoke tests
      |&lt;/li&gt;
&lt;li&gt;Green target group confirmed healthy
      |&lt;/li&gt;
&lt;li&gt;ALB routing switched
      |&lt;/li&gt;
&lt;li&gt;Traffic moves Blue -&amp;gt; Green
      |&lt;/li&gt;
&lt;li&gt;Monitor application + business metrics
      |&lt;/li&gt;
&lt;li&gt;Keep Blue available for rollback
      |&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Decommission Blue after confidence window&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Key Engineering Lessons&lt;br&gt;
13.1 Blue-Green Requires Coordinated Deployment Controls&lt;br&gt;
Blue-Green depends on the interaction between deployment orchestration, readiness, traffic management, resource versioning, CDN behavior, observability, and rollback. Two application environments alone do not address the dependencies across the release path.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;13.2 Move Preparation Work Outside the Cutover Path&lt;br&gt;
Image pulling, pod creation, startup, and readiness can take minutes. Blue-Green allows the previous version to continue serving traffic while the new environment completes this work.&lt;/p&gt;

&lt;p&gt;13.3 Rollback Should Be Designed Up Front&lt;br&gt;
Keeping the previous environment available allows the system to route traffic back to a known-good version without rebuilding and redeploying that version.&lt;/p&gt;

&lt;p&gt;13.4 Short Transition Windows Can Expose Race Conditions&lt;br&gt;
A short routing transition can still expose a race condition when request volume is high. Deployment design must account for requests that arrive while traffic moves between environments.&lt;/p&gt;

&lt;p&gt;13.5 Static Resources Are Part of the Release Architecture&lt;br&gt;
Frontend resources form part of the production deployment architecture. Their versioning, CDN lifetime, and relationship with the application shell must be considered as part of the release design.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Final Architecture&lt;/li&gt;
&lt;/ol&gt;

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

&lt;ol&gt;
&lt;li&gt;Conclusion
The move from rolling deployments to Blue-Green deployment addressed the version mismatch that could occur when build-specific frontend resources reached pods running an older build. Blue-Green separates release preparation from traffic cutover by allowing the new environment to complete startup, readiness checks, and validation while the existing environment continues serving customers.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The implementation showed that zero-downtime deployment depends on controls across the release path. Resource versioning, CDN caching, traffic switching, readiness checks, retry behavior, and rollback design must work together to protect customer requests during a release.&lt;/p&gt;

&lt;p&gt;Keeping the previous environment available also allows recovery through a traffic switch instead of another deployment. This release model reduces the need to schedule deployments around maintenance windows while preserving a path back to the previous version.&lt;/p&gt;

&lt;p&gt;Building Release Architectures for Production Reliability&lt;br&gt;
Zero-downtime deployment depends on more than switching traffic between two environments. Pod readiness, static-resource versioning, caching, traffic routing, &lt;a href="https://geekyants.com/engineering/devops" rel="noopener noreferrer"&gt;monitoring, and rollback&lt;/a&gt; must work together to protect customer-facing applications during a release. For teams building or modernizing applications with these production requirements, GeekyAnts provides &lt;a href="https://geekyants.com/service/custom-software-development-services" rel="noopener noreferrer"&gt;custom software development services&lt;/a&gt; covering architecture, CI/CD, deployment, and post-launch support.&lt;/p&gt;

&lt;p&gt;References&lt;br&gt;
Kubernetes: Liveness, Readiness, and Startup Probes&lt;br&gt;
&lt;a href="https://kubernetes.io/docs/concepts/workloads/pods/probes/" rel="noopener noreferrer"&gt;https://kubernetes.io/docs/concepts/workloads/pods/probes/&lt;/a&gt;&lt;br&gt;
AWS Elastic Load Balancing: Application Load Balancer&lt;br&gt;
&lt;a href="https://docs.aws.amazon.com/elasticloadbalancing/latest/application/introduction.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/elasticloadbalancing/latest/application/introduction.html&lt;/a&gt;&lt;br&gt;
AWS Elastic Load Balancing: Target Groups&lt;br&gt;
&lt;a href="https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-target-groups.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-target-groups.html&lt;/a&gt;&lt;br&gt;
AWS Elastic Load Balancing: Listener Rules&lt;br&gt;
&lt;a href="https://docs.aws.amazon.com/en_en/elasticloadbalancing/latest/application/listener-rules.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/en_en/elasticloadbalancing/latest/application/listener-rules.html&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>banking</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>The Deprecation Notice Nobody Reads</title>
      <dc:creator>GeekyAnts India Pvt Ltd</dc:creator>
      <pubDate>Wed, 07 Oct 2026 08:49:51 +0000</pubDate>
      <link>https://dev.to/geekyants/the-deprecation-notice-nobody-reads-1i96</link>
      <guid>https://dev.to/geekyants/the-deprecation-notice-nobody-reads-1i96</guid>
      <description>&lt;p&gt;This blog explains why API deprecation notices often fail and how staged retirement, telemetry, brownouts, and scheduled enforcement can make API migrations more controlled and predictable.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Every API team eventually writes the same message&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;"This endpoint will be removed on [date]. Please migrate before then." It goes in a changelog, a blog post, maybe an email. Traffic to the deprecated endpoint can show little movement until the day it is switched off, at which point support queues fill up with people who swear they never saw the notice.&lt;/p&gt;

&lt;p&gt;They may not have seen it. A warning with no consequence attached is easy for a busy team to defer. The problem with sunset timelines is not communication. Most contain nothing that creates a cost for delay until the exact day they stop being a warning and become an outage.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The flat line before the cliff&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The pattern shows up across &lt;a href="https://geekyants.com/hire-graphql-api-developers" rel="noopener noreferrer"&gt;&lt;strong&gt;API providers&lt;/strong&gt;&lt;/a&gt;: deprecation is announced, documentation is updated, and usage of the old endpoint barely dips. One cited account of a typical deprecation window reported that six months after announcing a sunset date, the old version was still carrying 40% of total traffic, with a team left asking why nobody had migrated.&lt;/p&gt;

&lt;p&gt;A deprecated endpoint that still returns 200 OK carries no urgency. Migration work competes with every other roadmap item, and "the thing that still technically works" reliably loses that competition until it stops working at all. A blog post is not an effective notice if ignoring it carries no immediate consequence.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Two timelines, two outcomes&lt;/strong&gt;
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Timeline&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Without a strategy&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;With a strategy&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Month 0&lt;/td&gt;
&lt;td&gt;Endpoint removed, no warning&lt;/td&gt;
&lt;td&gt;Deprecation announced; Deprecation/Sunset headers added; migration guide published&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Month 3&lt;/td&gt;
&lt;td&gt;47 integrations already broken&lt;/td&gt;
&lt;td&gt;Migration reminders sent to consumers still calling the endpoint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Month 6&lt;/td&gt;
&lt;td&gt;Support queue flooded&lt;/td&gt;
&lt;td&gt;First announced brownout window, a scheduled, published maintenance-style window during which the endpoint is intentionally degraded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Month 9&lt;/td&gt;
&lt;td&gt;Emergency rollback underway&lt;/td&gt;
&lt;td&gt;Quota tightened for remaining consumers; brownout windows lengthened per the published schedule&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Month 12&lt;/td&gt;
&lt;td&gt;Trust damaged, repeat incidents&lt;/td&gt;
&lt;td&gt;Endpoint retired, returns 410 Gone; near-zero breakage&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;What changes structurally is that, by month 6, the endpoint behaves differently for consumers who have not migrated. That change is published on a schedule before it takes effect rather than discovered by consumers in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Why longer notice periods don't fix this alone&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Some major API providers use deprecation or version-support windows measured in years, giving customers time to include migration work in planning cycles.&lt;/p&gt;

&lt;p&gt;But two years solves a planning problem, not an urgent problem. It does nothing to make migration feel pressing in month three, nine, or eighteen because nothing about the API has changed yet. The calendar moved; the consequence didn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Staged, announced retirement&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The fix isn't a louder notice, and it isn't quiet degradation either. It's a retirement plan the API enforces on a schedule the consumer knows about. Every stage is disclosed in the changelog and response headers before it takes effect rather than discovered in production.&lt;/p&gt;

&lt;p&gt;A staged retirement can follow five stages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Headers-only window. Deprecation and Sunset headers present on every response. No behavioral change. This window should be long enough to cover at least one full planning cycle for your consumers.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Warnings surfaced in-band. A deprecation notice is added to response bodies, or a Link header points to the migration guide, so tooling rather than memory catches it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Announced brownout windows. Published, scheduled windows (e.g., "the deprecated endpoint will return elevated latency and occasional 429s every Tuesday 2–4pm UTC starting date") during which the endpoint behaves like it will after full retirement. These are calendared and documented before they start, exactly like a maintenance window.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Progressively tighter quotas for remaining consumers, identified by telemetry, not by chance.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Hard retirement. 410 Gone, with the migration link still present.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;At every stage, a consumer hitting the new behavior can look at a public schedule and confirm exactly which stage is in effect and when the next one begins. Nothing about it is a surprise, and nothing about it is silent.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;What changes for internal versus public APIs&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;For an internal API with a handful of known consuming teams, the friction curve can be steep and communication can be direct. A Slack message plus a dashboard showing which service is still calling the old path can help get a fix shipped within a sprint. The consuming teams are known, and a sharper cutoff is more practical because the organization also controls the fallback.&lt;/p&gt;

&lt;p&gt;Public and partner-facing APIs are the harder case. The consumer may be unknown until they show up in usage telemetry, may not follow the changelog, and may be running an integration nobody at their organization has touched since it was built. That is where headers, published schedules, and graduated enforcement provide value. They turn an anonymous, unreachable consumer's client code into something that reports its own deprecation status back to whoever looks at the logs.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Comparing three retirement strategies&lt;/strong&gt;
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Communication-only&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Hard sunset&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Staged enforcement (brownouts)&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;What it is&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Changelog/email notice, no behavior change until removal day&lt;/td&gt;
&lt;td&gt;Fixed date, endpoint simply stops working&lt;/td&gt;
&lt;td&gt;Published schedule of increasing, announced friction before final removal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Best fit&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Low-risk internal APIs, few known consumers&lt;/td&gt;
&lt;td&gt;Emergency/security-driven deprecations&lt;/td&gt;
&lt;td&gt;Public and partner APIs with unknown or numerous consumers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Retry-storm risk&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Low until cutover, spikes hard at deadline&lt;/td&gt;
&lt;td&gt;High; everyone fails at once&lt;/td&gt;
&lt;td&gt;Distributed across scheduled windows; retry behavior can be observed before it's catastrophic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SLA / contractual exposure&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Rarely satisfies partner deprecation clauses alone&lt;/td&gt;
&lt;td&gt;Highest exposure if the date arrives early&lt;/td&gt;
&lt;td&gt;Lower; each stage documented and pausable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Rollback strategy&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Trivial (nothing changed yet)&lt;/td&gt;
&lt;td&gt;Difficult; re-enabling under load is its own incident&lt;/td&gt;
&lt;td&gt;Straightforward; each stage is a &lt;a href="https://geekyants.com/blog/feature-flags-as-technical-debt-the-cleanup-nobody-schedules" rel="noopener noreferrer"&gt;&lt;strong&gt;feature flag&lt;/strong&gt;&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Operational cost&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Low upfront, high at cutover&lt;/td&gt;
&lt;td&gt;Low upfront, very high at cutover&lt;/td&gt;
&lt;td&gt;Higher upfront, spread more evenly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;When it fails&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Consumers defer migration&lt;/td&gt;
&lt;td&gt;Consumers who missed the notice get zero warning&lt;/td&gt;
&lt;td&gt;Poorly tuned severity or cadence can trigger retries without Retry-After and backoff&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;No single strategy is correct in isolation. The choice depends on how well you know your consumers, what your contracts require, and how much operational machinery you're willing to build before you start the clock.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Standards-correct signals&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;RFC 9745 defines Deprecation as a Structured Fields Date, not an HTTP-date string. That means the value should look like @1735689600 (an @-prefixed Unix timestamp), not the output of toUTCString(). Sunset (RFC 8594) does use an HTTP-date. Mixing the two formats can break client tooling. A malformed value may fail to parse as expected, which can disrupt automated deprecation tracking.&lt;/p&gt;

&lt;p&gt;Production-oriented middleware, with per-consumer rollout, a rollback switch, and telemetry hooks built in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Formats a JS Date/timestamp as an RFC 9745 Structured Field Date: "@" function toStructuredFieldDate(msTimestamp) { return @${Math.floor(msTimestamp / 1000)}; }

function deprecationMiddleware({ deprecationDate, sunsetDate, migrationLink, stages, // ordered array of { startsAt, mode, config } isEnforced, // (consumerId) =&amp;gt; bool — per-consumer rollout / allowlist telemetry, // { recordHit(stage, consumerId), recordOutcome(stage, consumerId, result) } killSwitch, // () =&amp;gt; bool — global rollback }) { return function (req, res, next) { const now = Date.now(); const consumerId = req.consumerId; 

res.setHeader('Deprecation', toStructuredFieldDate(deprecationDate));
res.setHeader('Sunset', new Date(sunsetDate).toUTCString());
res.setHeader('Link', `&amp;lt;${migrationLink}&amp;gt;; rel="deprecation"`);

telemetry.recordHit('headers', consumerId);

if (killSwitch()) {
  return next(); // incident-response rollback: headers stay, no enforcement
}

const activeStage = stages
  .filter((s) =&amp;gt; now &amp;gt;= s.startsAt)
  .sort((a, b) =&amp;gt; b.startsAt - a.startsAt)[0];

if (!activeStage || !isEnforced(consumerId)) {
  return next();
}

switch (activeStage.mode) {
  case 'retired': {
    telemetry.recordOutcome('retired', consumerId, '410');
    return res.status(410).json({
      error: 'gone',
      message: 'This endpoint was retired. See the Link header for the migration guide.',
    });
  }
  case 'brownout': {
    const { rejectRatio, retryAfterSeconds } = activeStage.config;
    if (Math.random() &amp;lt; rejectRatio) {
      telemetry.recordOutcome('brownout', consumerId, '429');
      res.setHeader('Retry-After', String(retryAfterSeconds));
      return res.status(429).json({
        error: 'deprecated_brownout',
        message: `Scheduled brownout in effect per the published retirement schedule. See ${migrationLink}.`,
      });
    }
    telemetry.recordOutcome('brownout', consumerId, 'pass');
    return next();
  }
  case 'quota': {
    req.deprecationQuotaTag = activeStage.config.quotaKey;
    return next();
  }
  default:
    return next();
   }
 }; 
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stages here are explicit, scheduled, published data rather than a continuous random function tied to elapsed time. Enforcement is per-consumer and gated by an allowlist so a rollout can ramp in stages instead of switching on for every consumer at the same time. A kill switch supports incident response, and every enforcement decision is recorded to telemetry so it can feed the decision loop below.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Telemetry as the backbone&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;"Track usage per consumer" isn't a strategy on its own. The strategy is the decision loop that telemetry feeds:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Identify every consumer still hitting the deprecated path, from API keys/auth identity, not just aggregate request counts.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Segment by risk and traffic. A partner integration processing payments and a dormant internal script hitting the endpoint twice a day need different handling and different owners.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Notify owners, not just via the changelog. The segmented list makes targeted outreach possible.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Measure migration after each stage, before deciding to advance. The gate for moving to the next stage should be a measured drop in remaining traffic plus confirmed outreach, not just a date on a calendar.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Enforce the next stage only for consumers who haven't moved. Scope enforcement to the remaining, already-notified cohort rather than applying it to everyone the moment a date arrives.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This loop is also what tells you when to pause: if a critical, contractually-protected partner is still on the old path with an active migration in progress, the gate holds them back from the next enforcement stage while the rest of the cohort proceeds.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Testing the strategy before shipping it&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Before rolling out a staged retirement for a public or partner API, &lt;a href="https://geekyants.com/engineering/quality-assurance/api-testing" rel="noopener noreferrer"&gt;&lt;strong&gt;test the full sequence&lt;/strong&gt;&lt;/a&gt; of headers, warnings, one brownout window, quota tightening, and retirement against a low-traffic internal endpoint with a small, known set of consumers. Compress the sequence into a shorter window than a real public deprecation would use.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;What to Capture at Each Stage&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Request volume to the deprecated path, before and after each stage transition&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Number of distinct consumers still active, and how that count changes after outreach vs. after the first brownout&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Retry behavior during the brownout window, including whether clients back off per Retry-After or retry at a rate that begins to resemble an attack&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Support/Slack messages generated per stage&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Time from "notified" to "migrated" per consumer, segmented by whether they moved after notice alone or only after a brownout&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The test report should show the share of consumers who migrated after headers and direct outreach, the share who required a brownout window, the number of integrations requiring one-on-one follow-up, and any unexpected brownout behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What actually breaks&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Staged retirements do not fail cleanly, so teams need to plan for the ways they can fail.&lt;/p&gt;

&lt;p&gt;A brownout window can trigger a retry loop that amplifies load instead of shedding it if clients do not respect Retry-After. That is why the header has to be present on every throttled response, not just the first.&lt;/p&gt;

&lt;p&gt;A batch job can fail without drawing attention if it does not check for 429 at all. The problem may surface when someone notices missing data downstream.&lt;/p&gt;

&lt;p&gt;A dormant integration with no listed owner can remain invisible until an enforcement stage breaks it. That is why the consumer registry needs an ownership field checked before enforcement begins, not after.&lt;/p&gt;

&lt;p&gt;None of these are arguments against staged enforcement. They are arguments for building the telemetry and allowlist machinery described above before turning any stage on. If something behaves in an unexpected way, it should show up in a dashboard instead of a support ticket.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Why this matters now&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;API environments now include &lt;a href="https://geekyants.com/engineering/backend/enterprise-backend-development" rel="noopener noreferrer"&gt;&lt;strong&gt;internal microservice contracts&lt;/strong&gt;&lt;/a&gt;, partner integrations, and a growing layer of &lt;a href="https://geekyants.com/blog/ai-agents-the-next-frontier-in-intelligent-automation" rel="noopener noreferrer"&gt;&lt;strong&gt;AI agents&lt;/strong&gt;&lt;/a&gt; and tools that can call APIs without a person reading a changelog. A deprecation strategy that depends on a person noticing an email doesn't scale to that world. One built into the protocol through headers, a published schedule, and telemetry-gated enforcement does.&lt;/p&gt;

&lt;p&gt;Sunset timelines don't fail because nobody announced them. They fail because, for most of the window, nothing about using the deprecated path changed. The fix isn't writing a better warning, and it isn't making things worse without notice until the deadline. It's a retirement plan the API keeps, announced one stage at a time. By the retirement date, consumers should know it is coming because they have seen it on the schedule, not because they experienced the change without warning.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Build APIs for Change, Not Just Launch&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;API reliability extends beyond keeping an endpoint available. Teams also need a plan for how APIs evolve, how consumers migrate, and how older versions are retired without creating avoidable production failures. Staged retirement, telemetry, and controlled enforcement make deprecation part of the API lifecycle rather than a last-minute cutoff. Explore how GeekyAnts approaches scalable systems and API architecture through its &lt;a href="https://geekyants.com/engineering/backend" rel="noopener noreferrer"&gt;&lt;strong&gt;Backend Engineering&lt;/strong&gt;&lt;/a&gt; capabilities.&lt;/p&gt;

</description>
      <category>api</category>
      <category>webdev</category>
      <category>programming</category>
      <category>devops</category>
    </item>
    <item>
      <title>From Manual Testing to AI-Assisted Automation with Playwright Agents</title>
      <dc:creator>GeekyAnts India Pvt Ltd</dc:creator>
      <pubDate>Wed, 07 Oct 2026 06:01:38 +0000</pubDate>
      <link>https://dev.to/geekyants/from-manual-testing-to-ai-assisted-automation-with-playwright-agents-48n</link>
      <guid>https://dev.to/geekyants/from-manual-testing-to-ai-assisted-automation-with-playwright-agents-48n</guid>
      <description>&lt;p&gt;This blog discusses the value of Playwright Agents in automating workflows. It provides a detailed description of setting up the system, as well as a breakdown of the Playwright Agent’s automation process.&lt;/p&gt;

&lt;p&gt;For years, &lt;a href="https://geekyants.com/en-in/engineering/quality-assurance/qa-automation-testing" rel="noopener noreferrer"&gt;&lt;strong&gt;automation engineers&lt;/strong&gt;&lt;/a&gt; have followed a familiar rhythm. Requirements come in, test cases are written, scripts are automated, locators break, scripts fail, and debugging begins. Fix, re-run, repeat.&lt;/p&gt;

&lt;p&gt;This cycle hasn’t changed much, even though frameworks have evolved from Selenium to Cypress to modern tools like Playwright.&lt;/p&gt;

&lt;p&gt;What if your automation framework didn’t just execute tests — what if it planned them, wrote them, ran them, and even fixed them when they broke? That’s exactly what &lt;a href="https://geekyants.com/en-in/blog/how-ai-ml-are-transforming-quality-assurance-in-software-testing-with-playwright-examples" rel="noopener noreferrer"&gt;&lt;strong&gt;Playwright Test Agents&lt;/strong&gt;&lt;/a&gt; bring to the table.&lt;/p&gt;

&lt;p&gt;Playwright introduced these &lt;a href="https://geekyants.com/en-in/ai/ai-agent-development-services" rel="noopener noreferrer"&gt;&lt;strong&gt;AI-powered agents&lt;/strong&gt;&lt;/a&gt; in version 1.56 to automate key parts of the testing lifecycle — planning, generating, and healing tests — using an agentic loop that interacts with your live application.&lt;/p&gt;

&lt;p&gt;In this blog, we’ll explore what Playwright Agents are, how to set them up, how to use seed tests and prompts, what files they generate (like in your screenshot), and how each agent works in the development lifecycle. We’ll close with practical tips on prompt design and real differences versus generic &lt;a href="https://geekyants.com/en-in/blog/top-8-ai-coding-tools-for-developers-in-the-usa-2025-edition" rel="noopener noreferrer"&gt;&lt;strong&gt;AI coding tools&lt;/strong&gt;&lt;/a&gt; like Cursor or ChatGPT.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Evolution of Automation with Playwright&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Playwright became popular by addressing common automation challenges like flaky tests and synchronization issues. With features like automatic waiting, semantic locators such as &lt;strong&gt;getByRole&lt;/strong&gt;, and built-in tracing, it reduced the effort required to stabilize tests. This allowed QA engineers to focus more on test coverage rather than debugging framework issues. However, even with these improvements, designing and maintaining test scripts still remained a manual effort.&lt;/p&gt;

&lt;p&gt;We still had to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Translate requirements into scenarios&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Convert scenarios into code&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Refactor when UI changes&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fix broken locators&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Playwright Agents aim to assist in exactly those areas.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Introducing Playwright Test Agents&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Playwright Test Agents are &lt;a href="https://geekyants.com/en-in/blog/revolutionizing-business-process-automation-with-ai-agents" rel="noopener noreferrer"&gt;&lt;strong&gt;AI-assisted automation workflows&lt;/strong&gt;&lt;/a&gt; embedded directly into your Playwright project, designed to help you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Explore an application and produce a test plan&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Transform that plan into executable Playwright test code&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Run tests and automatically repair failures&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are three core agents:&lt;/p&gt;

&lt;p&gt;The Planner Agent takes natural language input and converts it into structured test scenarios. It uses the seed test as context to explore the application, understand user flows, and identify possible edge cases. The output is a detailed test plan with steps and expected outcomes, similar to how a &lt;a href="https://geekyants.com/en-in/service/hire-quality-assurance-developers" rel="noopener noreferrer"&gt;&lt;strong&gt;QA engineer&lt;/strong&gt;&lt;/a&gt; would design test cases.&lt;/p&gt;

&lt;p&gt;The Generator Agent takes these structured scenarios and converts them into executable Playwright scripts. While generating code, it interacts with the live application to validate selectors, identify stable locators, and ensure assertions reflect actual UI behavior. It can also follow architectural patterns like Page Object Model, producing manageable and scalable test code.&lt;/p&gt;

&lt;p&gt;The Healer Agent focuses on maintaining test stability. When a test fails, it replays the scenario, inspects the DOM, and identifies what caused the failure. It then attempts to fix the issue by updating selectors, adjusting waits, or modifying interaction logic, reducing the manual effort required for test maintenance.&lt;/p&gt;

&lt;p&gt;These agents can be invoked independently or chained together in a complete “agentic loop”:&lt;/p&gt;

&lt;p&gt;Planner → Generator → Healer&lt;/p&gt;

&lt;p&gt;This turns a natural language description of test requirements into a stable test suite with minimal manual coding.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Project Setup — Step by Step (Beginner Friendly)&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;If you already know basic &lt;a href="https://geekyants.com/en-in/blog/automation-testing-with-playwright-using-javascript" rel="noopener noreferrer"&gt;&lt;strong&gt;Playwright automation&lt;/strong&gt;&lt;/a&gt;, this should feel like an extension of that knowledge. If not, stick with it. By the end, you will understand how these agents help even if you’re new to automation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Create a Playwright Project&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Start by creating a new directory and initializing a Node project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mkdir playwright-agent-demo
cd playwright-agent-demo
npm init -y
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install -D @playwright/test
npx playwright install
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Now install Playwright:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You now have a basic Playwright project.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;2. Initialize Playwright Agents&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;To add agent definitions to your project, run:&lt;/p&gt;

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

&lt;p&gt;This command generates agent files in your project — which you’ll recognize in the screenshot below:&lt;/p&gt;

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

&lt;p&gt;A folder named &lt;strong&gt;.github/agents&lt;/strong&gt; contains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;playwright-test-planner.agent.md&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;playwright-test-generator.agent.md&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;playwright-test-healer.agent.md&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are the agent definitions that your AI tool (like Claude Code, VS Code Copilot, or OpenCode) uses to understand how to plan, generate, and heal tests.&lt;/p&gt;

&lt;p&gt;Under the root folder, you also see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;specs/&lt;/strong&gt; – for Markdown plans&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;tests/&lt;/strong&gt; – for generated Playwright test files&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;seed.spec.ts&lt;/strong&gt; – a seed test that bootstraps the environment&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This exact file structure is aligned with Playwright’s agent conventions: &lt;strong&gt;.github/agents&lt;/strong&gt;, &lt;strong&gt;specs/&lt;/strong&gt;, and &lt;strong&gt;tests/&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A seed test is essential because it provides a starting context that the planner uses to understand where to begin exploration, including any setup required (like logging in or navigating to a landing page).&lt;/p&gt;

&lt;p&gt;Create &lt;strong&gt;tests/seed.spec.ts&lt;/strong&gt;:&lt;/p&gt;

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

&lt;p&gt;Next, add dummy seed data to a JSON file (optional but recommended):&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;testdata/seed.json&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;The seed test and seed data help the Planner understand context and scenarios, which makes its output far more relevant and accurate.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;How the Planner Agent Works&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The Planner Agent is like a QA analyst powered by &lt;a href="https://geekyants.com/en-in/ai" rel="noopener noreferrer"&gt;&lt;strong&gt;AI&lt;/strong&gt;&lt;/a&gt;. Rather than immediately writing code, it first produces a structured Markdown test plan that describes required test scenarios, user flows, steps, expected outcomes, and test data.&lt;/p&gt;

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

&lt;p&gt;You can review this file before moving to code generation.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Generator Agent: Turning Plans into Code&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Once you have a test plan, it’s time to generate actual automation scripts.&lt;/p&gt;

&lt;p&gt;Switch your &lt;a href="https://geekyants.com/en-in/artificial-intelligence-consulting" rel="noopener noreferrer"&gt;&lt;strong&gt;AI assistant&lt;/strong&gt;&lt;/a&gt; to Generator mode and provide a prompt such as:&lt;/p&gt;

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

&lt;p&gt;The Generator reads the Markdown plan, actively interacts with the browser to verify selectors and assertions, and produces test scripts under the &lt;strong&gt;tests/&lt;/strong&gt; directory. Similar to this:&lt;/p&gt;

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

&lt;p&gt;For example, provide a prompt like:&lt;/p&gt;

&lt;p&gt;“Create a test plan for login functionality with valid and invalid user scenarios using the seed test context.”&lt;/p&gt;

&lt;p&gt;The Planner will explore your live application (through the seed test) and generate a Markdown file under s*&lt;em&gt;pecs/&lt;/em&gt;* such as:&lt;/p&gt;

&lt;p&gt;specs/login-plan.md&lt;/p&gt;

&lt;p&gt;This file contains detailed, human-readable test plans, not code, but instructions for how you want the generator to build tests.&lt;/p&gt;

&lt;p&gt;This step mirrors the typical QA process of writing test case documentation, except that the agent generates it automatically.&lt;/p&gt;

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

&lt;p&gt;Each test should mirror a scenario from the plan.&lt;/p&gt;

&lt;p&gt;Because the Generator interacts directly with the live app and evaluates selectors as it writes code, the tests it generates are often more stable and accurate than typical prompt-only AI output.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Healer Agent: Using AI to Fix Failing Tests&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Inevitably, tests fail. It might be due to UI changes, such as an updated locator or changed button label.&lt;/p&gt;

&lt;p&gt;Traditionally, you would open your editor, inspect the DOM, update selectors, and re-run tests. With Playwright’s Healer Agent, this can be assisted by AI.&lt;/p&gt;

&lt;p&gt;Invoke the healer with a prompt like:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;“Run and fix the failing test tests/amazon-search-add-to-cart-edge.spec.spec.ts.”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The healer will:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Replay the failing test in debug mode&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Inspect the DOM to find equivalent elements or flows&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Propose updates to locators or waits&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Re-run until the test passes, or decide that the test really reflects a broken feature.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This reduces repeated manual debugging cycles, especially for tests that only break due to minor UI refactors.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;From Prompt to Execution: Inside the Agent Workflow&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;While using Playwright Test Agents feels simple from a user perspective, there is significant processing happening in the background.&lt;/p&gt;

&lt;p&gt;The agents operate through an agentic loop where they can read project files, execute tests, interact with the browser, and inspect the live DOM. For example, the Planner uses the seed test to explore the application and understand flows, the Generator validates selectors in real time while generating scripts, and the Healer replays failing tests to identify and fix issues.&lt;/p&gt;

&lt;p&gt;In the foreground, this complexity is abstracted into simple inputs and outputs. Users provide prompts and receive structured test plans, executable test scripts, or suggested fixes without directly interacting with the underlying processes. This separation is what makes Playwright Agents both powerful and easy to use.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Structuring Tests with Page Object Model&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;One of the biggest benefits of designer prompts is instructing the generator to produce maintainable code, and that starts with architecture.&lt;/p&gt;

&lt;p&gt;If you prompt:&lt;/p&gt;

&lt;p&gt;“Use Page Object Model and store locators in separate page files.”&lt;/p&gt;

&lt;p&gt;The generator will output something like:&lt;/p&gt;

&lt;p&gt;pages/login.page.ts&lt;/p&gt;

&lt;p&gt;tests/login/login.spec.ts&lt;/p&gt;

&lt;p&gt;Where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;login.page.ts&lt;/strong&gt; contains locator definitions and reusable page actions&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;login.spec.ts&lt;/strong&gt; uses the page object and seed data for test logic&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This results in a clean, maintainable automation framework that scales well.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Agentic Loop: From Plan to Stable Tests&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;When you use all three agents together, you get:&lt;/p&gt;

&lt;p&gt;Seed Test + Prompt&lt;/p&gt;

&lt;p&gt;↓&lt;/p&gt;

&lt;p&gt;Planner → Create Markdown Plan&lt;/p&gt;

&lt;p&gt;↓&lt;/p&gt;

&lt;p&gt;Generator → Create Tests&lt;/p&gt;

&lt;p&gt;↓&lt;/p&gt;

&lt;p&gt;Healer → Fix Failures&lt;/p&gt;

&lt;p&gt;↓&lt;/p&gt;

&lt;p&gt;Stable Automation Suite&lt;/p&gt;

&lt;p&gt;This mirrors a full human automation lifecycle, except now it is assisted by AI and deeply integrated with Playwright’s tooling.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;How Playwright Agents Differ from Generic AI Tools&lt;/strong&gt;
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Regular AI Code Generation&lt;/th&gt;
&lt;th&gt;Playwright Agents&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Generates code based on prompts&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runs tests&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fixes failing tests autonomously&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Understands live DOM while generating code&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Integrated into the Playwright ecosystem&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;It’s easy to confuse Playwright Agents with other AI coding tools, such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Cursor AI&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;ChatGPT code generation&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Generic AI assistants&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But there is a fundamental difference:&lt;/p&gt;

&lt;p&gt;Playwright Agents integrate with MCP (Model Context Protocol) and interact with your application and tests as part of the lifecycle. This makes them far more context-aware and useful than simple prompt-to-code generation.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Best Practices for Using Playwright Test Agents&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Here are some practical tips based on real usage trends:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Provide Good Context&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Always include a clear seed test&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use structured seed data&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Reference environment details&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Write Clear Prompts&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Make sure your prompts include architecture preferences, test data references, and expected outputs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Review Generated Tests&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;AI can generate great boilerplate, but human review is still important.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Integrate into CI Carefully&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Treat healed and generated tests as drafts until fully reviewed.&lt;/p&gt;

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

&lt;p&gt;Playwright Agents, Planner, Generator, and Healer bring AI directly into the automation lifecycle. They:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Plan test scenarios from natural language&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Generate well-structured automation code&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Check and repair failing tests&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Help QA teams move faster with less manual overhead&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For any QA engineer with basic Playwright knowledge, these agents unlock productivity leaps, from planning without code to generating and healing tests with AI.&lt;/p&gt;

&lt;p&gt;If you want to experiment with this in your own project, run the agent setup, build a seed test, and start with simple prompts. You will be amazed at how much of the automation lifecycle can now be AI-assisted.&lt;/p&gt;

</description>
      <category>technology</category>
      <category>ai</category>
      <category>testing</category>
      <category>qualityassurance</category>
    </item>
    <item>
      <title>From Prompting to Process: What Changed When Flutter Shipped Agent Skills</title>
      <dc:creator>GeekyAnts India Pvt Ltd</dc:creator>
      <pubDate>Mon, 28 Sep 2026 11:44:01 +0000</pubDate>
      <link>https://dev.to/geekyants/from-prompting-to-process-what-changed-when-flutter-shipped-agent-skills-38p9</link>
      <guid>https://dev.to/geekyants/from-prompting-to-process-what-changed-when-flutter-shipped-agent-skills-38p9</guid>
      <description>&lt;p&gt;Google is starting to ship Flutter's engineering workflows as machine-readable guidance for AI agents. It may look like another AI feature, but it hints at a much bigger shift in how teams build with Flutter.&lt;/p&gt;

&lt;p&gt;The Consistency Problem&lt;/p&gt;

&lt;p&gt;Over the past few years, AI coding tools like Cursor, Claude Code, Copilot, and OpenCode have become part of many developers' daily workflows. They produce code, explain unfamiliar APIs, write tests, and navigate large codebases with impressive accuracy.&lt;/p&gt;

&lt;p&gt;But if you have been using them on a reasonably large project, you have probably noticed something.&lt;/p&gt;

&lt;p&gt;They are not very consistent.&lt;/p&gt;

&lt;p&gt;Ask an AI to implement the same feature in two different sessions, and there's a good chance you will get two different approaches. Switch models, and the implementation changes again. Sometimes it follows the latest framework recommendations. Other times, it relies on outdated patterns.&lt;/p&gt;

&lt;p&gt;Most teams respond the same way: write better prompts, add repository rules, or create a custom skill.md files to steer the agent toward the right decisions.&lt;/p&gt;

&lt;p&gt;We took the same approach by documenting our architecture, coding conventions, review expectations, and engineering practices as custom Skills. They made the agent noticeably more consistent.&lt;/p&gt;

&lt;p&gt;So when Flutter announced official Agent Skills, my first reaction was not: "How do we use them?" It was: "If we already have our own Skills, what problem are Flutter's official Skills actually solving?"&lt;/p&gt;

&lt;p&gt;That question turned into a couple of experiments.&lt;/p&gt;

&lt;p&gt;Three Layers of AI-Assisted Development&lt;/p&gt;

&lt;p&gt;Looking back, Flutter's recent AI investments weren't isolated features. They were building on each other.&lt;/p&gt;

&lt;p&gt;First came Rules, giving teams a way to define project-specific conventions and preferences.&lt;/p&gt;

&lt;p&gt;Then came Model Context Protocol (MCP), allowing AI agents to inspect, debug, and interact with running Flutter applications instead of reasoning purely from static code.&lt;/p&gt;

&lt;p&gt;And then came Agent Skills.&lt;/p&gt;

&lt;p&gt;If Rules tell an agent how your team works, and MCP tells it what's happening inside your application, Agent Skills answer a different question: how does Flutter itself recommend solving this problem?&lt;/p&gt;

&lt;p&gt;That is the important shift.&lt;/p&gt;

&lt;p&gt;Flutter is now versioning its engineering workflows alongside the framework itself. Instead of relying entirely on what an AI model happened to learn during training, agents can follow workflows maintained by the Flutter team.&lt;/p&gt;

&lt;p&gt;Today, those workflows cover areas like:&lt;/p&gt;

&lt;p&gt;Localization&lt;br&gt;
Responsive layouts&lt;br&gt;
Routing&lt;br&gt;
Widget and unit testing&lt;br&gt;
Static analysis&lt;br&gt;
JSON serialization&lt;br&gt;
Platform integration&lt;br&gt;
Architecture best practices&lt;/p&gt;

&lt;p&gt;In other words, Flutter is shipping its engineering knowledge as structured workflows.&lt;/p&gt;

&lt;p&gt;That naturally led to the next question: does this actually change how an AI agent behaves?&lt;/p&gt;

&lt;p&gt;I ran two experiments to find out.&lt;/p&gt;

&lt;p&gt;First Experiment: Declarative Routing&lt;/p&gt;

&lt;p&gt;It is one of those features where there is not just one thing to do. Depending on the prompt, an AI could jump straight into writing routes, miss platform-specific configuration, skip deep linking altogether, or recommend an approach based on what it learned during training rather than Flutter's latest guidance.&lt;/p&gt;

&lt;p&gt;So I kept the prompt intentionally simple:&lt;/p&gt;

&lt;p&gt;"Set up declarative routing for this Flutter application."&lt;/p&gt;

&lt;p&gt;I wanted to see how the agent would approach the problem before I told it how to solve it.&lt;/p&gt;

&lt;p&gt;The first thing it did caught my attention.&lt;/p&gt;

&lt;p&gt;Before generating an implementation plan, it explicitly selected the flutter-setup-declarative-routing skill.&lt;/p&gt;

&lt;p&gt;Show Image&lt;/p&gt;

&lt;p&gt;From that point on, it was not about figuring out a solution; it was following Flutter's own workflow.&lt;/p&gt;

&lt;p&gt;That was the interesting part.&lt;/p&gt;

&lt;p&gt;Without Agent Skills, the implementation depends on the model's reasoning and whatever Flutter knowledge it has internalized. With Agent Skills, the framework itself becomes the starting point.&lt;/p&gt;

&lt;p&gt;What happens when Flutter's Skills and our own Skills are both available?&lt;/p&gt;

&lt;p&gt;I tested this with a login screen.&lt;/p&gt;

&lt;p&gt;It needed:&lt;/p&gt;

&lt;p&gt;Flutter's localization workflow&lt;br&gt;
Our project conventions for authentication, repositories, dependency injection, and state management&lt;/p&gt;

&lt;p&gt;I kept the prompt simple and let the agent decide how to approach it -&lt;/p&gt;

&lt;p&gt;"Implement a login screen for this application. Follow the existing project architecture and add localization for all user-facing strings.&lt;/p&gt;

&lt;p&gt;It picked the right source every time.&lt;/p&gt;

&lt;p&gt;For localization, it used Flutter's official Skill. For everything related to our application architecture, it followed our custom Skills. I did not have to tell it which one to use or write a carefully engineered prompt.&lt;/p&gt;

&lt;p&gt;The two sets of Skills worked together naturally. Flutter handled the framework guidance, while our repository continued to define how our application was built.&lt;/p&gt;

&lt;p&gt;Show Image&lt;/p&gt;

&lt;p&gt;Why This Matters: A Floor, Not a Finish Line&lt;/p&gt;

&lt;p&gt;So, what actually changed?&lt;/p&gt;

&lt;p&gt;Flutter is taking ownership of its engineering expertise.&lt;/p&gt;

&lt;p&gt;Engineering teams no longer need to teach AI how Flutter expects routing, localization, responsive layouts, or testing to be implemented. Flutter now ships that knowledge itself.&lt;/p&gt;

&lt;p&gt;That removes a significant amount of duplication. Instead of every team maintaining its own version of Flutter best practices inside prompts or custom Skills, the official workflows become the source of truth.&lt;/p&gt;

&lt;p&gt;Even better, those workflows evolve alongside Flutter. As recommendations change, the official Skills change too, and every compatible AI agent benefits automatically.&lt;/p&gt;

&lt;p&gt;Of course, that's only half the story.&lt;/p&gt;

&lt;p&gt;Flutter's Agent Skills provide a strong foundation, but they do not replace your organization's architecture, coding standards, or business-specific workflows. Those remain your responsibility, and that is exactly how it should be.&lt;/p&gt;

&lt;p&gt;Final Thoughts&lt;/p&gt;

&lt;p&gt;Looking back, Flutter's recent AI features fit together surprisingly well.&lt;/p&gt;

&lt;p&gt;Rules capture how your team works. MCP gives agents runtime context. Agent Skills teach them how Flutter itself expects problems to be solved. Custom Skills layer on everything unique to your organization.&lt;/p&gt;

&lt;p&gt;Together, they reduce the amount of engineering knowledge an AI has to infer. That is the real significance of Flutter's recent AI investments. Engineering knowledge is becoming explicit, versioned, reusable, and maintained by the people best positioned to own it.&lt;/p&gt;

&lt;p&gt;Today's skill library covers foundational workflows, but it already hints at what's possible. Imagine Skills for performance profiling, DevTools workflows, accessibility audits, plugin development, migrations, or advanced rendering patterns. Every new skill moves another piece of framework knowledge out of documentation and into a reusable workflow.&lt;/p&gt;

&lt;p&gt;The answer to how much of Flutter an agent should really guess keeps shrinking with every release.&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>ai</category>
      <category>promptengineering</category>
      <category>agents</category>
    </item>
    <item>
      <title>Feature Flags as Technical Debt: The Cleanup Nobody Schedules</title>
      <dc:creator>GeekyAnts India Pvt Ltd</dc:creator>
      <pubDate>Mon, 28 Sep 2026 08:46:47 +0000</pubDate>
      <link>https://dev.to/geekyants/feature-flags-as-technical-debt-the-cleanup-nobody-schedules-1j9o</link>
      <guid>https://dev.to/geekyants/feature-flags-as-technical-debt-the-cleanup-nobody-schedules-1j9o</guid>
      <description>&lt;p&gt;Feature flags are one of the cheapest tools in engineering to adopt and one of the most expensive to leave unmanaged. Teams use them for gradual rollouts, A/B tests, kill switches, and gating unfinished work, but few teams have a matching process for removing them once they have served their purpose. As a result, flags that were meant to be temporary become permanent, adding unnecessary complexity to the codebase.&lt;/p&gt;

&lt;p&gt;This article explains why flag cleanup gets skipped and why it creates a real engineering cost. It also walks through a staleness-detection implementation, a flag-removal exercise, and a practical checklist for managing flags across their full lifecycle, from creation to removal.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Why Flags Are Easy to Add and Hard to Remove&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Adding a flag is a small, fast PR. Wrapping a block of code in a conditional and wiring it to a config value takes minutes, and it ships in the same PR as the feature it gates — there's no separate approval step, no extra review, no reason for anyone to push back. Removing a flag involves a different kind of work. You have to find every place it is checked, including references in logging, analytics, and monitoring code, confirm which branch is now permanently "on" or "off," delete the dead branch, update or delete the tests that covered it, and verify that nothing downstream depends on the old behavior. This multi-step effort competes with new feature work for the same sprint capacity, so cleanup often gets pushed to a later sprint until the flag becomes a permanent part of the codebase.&lt;/p&gt;

&lt;p&gt;There's also a confidence problem. Once a flag has been live for months, the person who added it may have moved teams, changed roles, or forgotten why it was added. Removing the conditional can feel risky when its dependencies are unclear, so teams may leave it in place. That decision can turn a two-week release flag into a permanent part of the codebase.&lt;/p&gt;

&lt;p&gt;Underneath both of these is a structural gap: a flag rarely comes with a ticket, an expiry date, or a named owner responsible for its removal. By default, creating a flag does not create a corresponding task to revisit it later. Without documented future work, the flag can remain in place. Meanwhile, teams add new flags every sprint while removing few of the old ones, so the backlog grows and cleanup becomes more complex as additional flags enter the same code paths.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Flag Lifecycle&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The diagram below lays out the full path a flag should take, from creation to retirement. The critical fork sits in the middle of the diagram: once a flag reaches "stable at 100%," it either gets caught by an automated staleness check and routed to a removal PR, or it gets ignored and drifts into permanent technical debt. Most flags fail at exactly this point — not because removal is technically difficult, but because nothing in the default engineering workflow forces the question to be asked. Without an automated trigger, a flag can sit at "stable" for years with nobody ever explicitly deciding to leave it that way; it simply never comes up.&lt;/p&gt;

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

&lt;p&gt;Figure 1: The feature flag lifecycle, from creation through rollout to either scheduled removal or stale limbo.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Why This Is a Real Cost, Not Just Clutter&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Each independent binary flag can double the number of possible runtime states. A function gated by three independent flags can have up to eight possible states, but teams may have designed or &lt;a href="https://geekyants.com/engineering/quality-assurance/functional-testing" rel="noopener noreferrer"&gt;&lt;strong&gt;tested&lt;/strong&gt;&lt;/a&gt; only a subset of those combinations. The remaining states can introduce interactions that were not considered during development or testing.&lt;/p&gt;

&lt;p&gt;Stale flags can create bugs through untested interactions. Two flags that are each considered "basically always on" can still interact in a combination the team did not anticipate. If the team assumes that only one meaningful state is live, that interaction may not be covered during testing or code review and can surface only under production traffic.&lt;/p&gt;

&lt;p&gt;New engineers may avoid code affected by unfamiliar flags. When it is unclear which flags are critical and which can be removed, team members may work around that code rather than risk breaking an unknown dependency. This can slow down simple changes and introduce workarounds that add further complexity.&lt;/p&gt;

&lt;p&gt;The number of dependencies can grow the longer a flag remains in place. Flag checks can extend into related systems, including analytics events tied to flag state, log lines that reference the flag, monitoring dashboards built around it, and configurations in other services. As these dependencies accumulate, removing the flag can require changes across several parts of the system.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Not All Flags Are the Same&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;A major reason cleanup gets mishandled is that teams treat every flag the same, even though the appropriate lifespan depends on why the flag exists. Classifying a flag at creation establishes its expected lifespan and removal requirements before the original context is lost.&lt;/p&gt;

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

&lt;p&gt;A &lt;strong&gt;release flag&lt;/strong&gt; usually lasts from a few days to a few weeks and is used to gate an in-progress feature during development and rollout. Its cleanup urgency is high, so it should be removed once the feature has reached 100% rollout and the release has stabilized. An &lt;strong&gt;experiment flag&lt;/strong&gt; typically remains active only for the duration of a test, such as an A/B experiment or a gradual, data-driven rollout. Its cleanup urgency is also high, and it should be removed as soon as the experiment concludes, regardless of the outcome. An &lt;strong&gt;ops or kill-switch flag&lt;/strong&gt;, by contrast, is designed to remain in place indefinitely. It acts as a manual override for risky dependencies or emergency controls, so it does not need immediate removal. Instead, it should be reviewed periodically to confirm that it is still necessary and working as intended.&lt;/p&gt;

&lt;p&gt;This highlights a common cleanup problem: a release or experiment flag with a short intended lifespan can receive the same caution as an ops kill switch designed to remain in place. That mismatch can turn a two-week flag into a permanent one.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Building the Staleness Detector&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The checklist below recommends automating staleness detection, so it helps to show what that implementation looks like. The following example presents the core logic, simplified for readability. In practice, it receives data from the flag provider in use, such as LaunchDarkly, Unleash, or a homegrown configuration table, with each flag providing a rollout percentage, type, and the length of time its state has remained unchanged.&lt;/p&gt;

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

&lt;p&gt;Two design choices here matter more than the code itself:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Thresholds are per-type, not global.&lt;/strong&gt; A single "flag unchanged for 90 days" rule either fires constantly on ops flags that are correctly untouched, or lets release flags rot for far too long. Splitting the threshold by type — pulled directly from the classification the team should already be doing at creation — makes the report something people trust instead of something they learn to ignore.&lt;/p&gt;

&lt;p&gt;The report identifies UNASSIGNED owners. An unowned stale flag has no person or team responsible for acting on the report, which can leave it in the codebase without a clear path to removal. Surfacing that ownership gap instead of defaulting to "team lead" makes responsibility for cleanup visible.&lt;/p&gt;

&lt;p&gt;Wiring this into a weekly Slack post or a lightweight internal dashboard, including a spreadsheet as a starting point, turns staleness into something the team reviews on a fixed cadence rather than discovers during an unrelated investigation.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Removing a Stale Flag: A Worked Example&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Detection is only half the problem. Deleting a flag safely requires a defined procedure because missed dependencies can create production issues. The sequence below shows how to remove a release flag that has remained at 100% for several weeks, using a checkout-discount flag as an example.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Confirm the resolved state, not just the current one.&lt;/strong&gt; Check the flag's rollout history, not just its current value — a flag sitting at 100% today that was flipped back to 0% twice in the last month is not actually stable, regardless of what the staleness report says this week.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Search for every reference, not just the obvious one.&lt;/strong&gt; A project-wide search for the flag's key (checkout_discount_v2) can reveal references beyond the if branch in the checkout service, including a log line that prints the flag's value, an analytics event property, and a conditional in a monitoring dashboard's alert query. All of these references need to be accounted for so the cleanup does not leave dead references behind.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Delete the dead branch, not just the flag check.&lt;/strong&gt; Before removal, the code checks the flag and branches between apply_discount_v2(cart) and apply_discount_legacy(cart). After removal, it calls apply_discount_v2(cart) directly, with both the flag check and the unused branch removed. Leaving both branches as dead code "just in case" preserves unnecessary code outside the flag inventory. If apply_discount_legacy has no other callers, remove it as well.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Remove the tests for the dead branch, not just add tests for the surviving one.&lt;/strong&gt; The legacy-path test coverage is now testing code that no longer exists in any reachable state; leaving it in the suite either silently rots (mocking a function that's been deleted) or keeps a maintenance burden alive for behavior nobody can trigger anymore.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Ship it as its own PR, reviewed as a deletion.&lt;/strong&gt; Bundling flag removal into an unrelated feature PR can reduce the attention given to the search results from step 2. A standalone "remove checkout_discount_v2" PR keeps the review focused on deletion. Any addition in that diff warrants additional review. This procedure is often undocumented, which can make cleanup PRs feel riskier than they are.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  When Cleanup Doesn't Happen: A Technical Walkthrough
&lt;/h2&gt;

&lt;p&gt;The scenario below illustrates a failure pattern common enough that most teams running flags at any scale will recognize a version of it, even if the specifics differ. A team adds checkout_discount_v2 to gate a new checkout flow. The rollout succeeds within two weeks and reaches 100% — by every functional measure, the flag has done its job. But it stays in the code for over a year, because removal was never written into any ticket and nobody was assigned to come back to it.&lt;/p&gt;

&lt;p&gt;Months later, a second flag, checkout_pricing_experiment, is added to the same checkout path for an unrelated pricing test. It runs after the discount flag: it takes whatever total the discount logic produced and applies an experimental pricing adjustment on top. Individually, each flag was tested and behaved correctly. However, apply_experimental_pricing was written and reviewed under the assumption that total came from apply_discount_legacy. By that point, checkout_discount_v2 had remained at 100% long enough that engineers working on the checkout path no longer considered it a meaningful variable, even though it remained in the code and continued to execute.&lt;/p&gt;

&lt;p&gt;apply_discount_v2 returned a total that had already been floored to two decimal places; apply_discount_legacy had not. apply_experimental_pricing applied a percentage multiplier and then rounded. This worked with apply_discount_legacy's unrounded output but could produce an off-by-one-cent total with apply_discount_v2's pre-rounded output for a narrow set of cart values. This type of bug can escape testing when no test covers the combination of two flags operating on the same code path. The fix required a two-line change to apply rounding consistently in one place. The greater cost came from the bug reaching production and requiring someone to identify a cent-level discrepancy in reconciliation data and trace it through a code path that was not expected to contain two active flags. This is the pattern the checklist below is designed to prevent: the gradual accumulation of untracked complexity that can lead to production issues.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Build vs. Buy: Tooling Trade-offs&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Teams generally use one of three approaches for flag management, and the right choice depends less on team size than on how comfortable the organization is with an external SaaS dependency in the request path.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;managed feature flag platform&lt;/strong&gt;, such as LaunchDarkly, provides rich targeting rules, built-in audit logs and change history, and features for identifying stale flags or monitoring usage with relatively little setup. The tradeoff is recurring cost, which can increase with seats or monthly active users. It also introduces another network dependency into the request path, and while the platform may identify stale flags, someone still needs to own and act on that cleanup.&lt;/p&gt;

&lt;p&gt;An &lt;strong&gt;open-source, self-hosted platform&lt;/strong&gt;, such as Unleash, avoids per-seat licensing costs and gives teams greater control over data residency and customization. However, the engineering team becomes responsible for operating the flag service, including uptime, upgrades, maintenance, and scaling. These platforms may also provide fewer built-in insights than managed alternatives unless additional monitoring and reporting are configured.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;homegrown configuration table&lt;/strong&gt; keeps the setup simple because it does not require introducing a separate feature flag platform. Teams can query the data directly and build custom staleness reports, such as the automated cleanup check described above. The downside is that there is usually no dedicated interface or audit trail unless those capabilities are built intentionally. Targeting logic can also spread across the codebase over time if it is not kept centralized.&lt;/p&gt;

&lt;p&gt;The staleness detector shown earlier works with all three approaches because it requires only a list of flags with a rollout percentage and a last-modified timestamp. This keeps the implementation compatible with each approach. Managed platforms may expose this information through a report or webhook, while the other two approaches can use the script above or a similar implementation. The tooling decision affects how much the team needs to build, but each approach still requires a process for detecting stale flags and assigning cleanup work.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;A Practical Checklist for Managing Flag Lifecycle&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Each step below maps directly to a stage in the feature flag lifecycle. Together, they turn the lifecycle from a diagram into a process that teams can follow consistently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Classify at creation.&lt;/strong&gt; Every flag should be tagged as a release, experiment, or ops flag when it is created. This establishes the expected lifespan and removal requirements from the beginning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Assign an owner and expiry.&lt;/strong&gt; Each flag should have a named person or team responsible for it, along with a target removal date, even if that date is approximate. Without clear ownership or a deadline, a flag can remain in the codebase indefinitely without a defined cleanup path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Bake removal into the ticket.&lt;/strong&gt; Flag removal should be included in the original story's definition of done. This prevents cleanup from becoming a separate task that has to compete for priority later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Automate staleness detection.&lt;/strong&gt; Teams should run scheduled checks for flags that have remained at 0% or 100% rollout beyond their type-specific staleness threshold. This makes stale flags visible without relying on someone to remember them manually.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Maintain a flag inventory.&lt;/strong&gt; A central dashboard should track every live flag, including its type, owner, and age. This gives teams a quick answer to questions such as how many flags are active and which ones may need attention.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Establish a recurring review cadence.&lt;/strong&gt; Teams should review flag ownership and staleness data monthly or quarterly. Regular reviews help catch neglected flags before they accumulate into a larger cleanup backlog.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Prioritize removal PRs.&lt;/strong&gt; Pull requests that remove obsolete flags should be treated as focused cleanup work. Because these changes primarily remove code rather than add new behavior, they can often have a narrower review scope while reducing unnecessary complexity in the codebase.&lt;/p&gt;

&lt;p&gt;A note on step 2: individual ownership can become outdated when the named owner changes teams or leaves the company. Tying ownership to a service or feature area rather than a specific person provides continuity when personnel change and reduces the risk of flags becoming orphaned.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Closing Thought&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Feature flags are useful, but flag creation represents only part of their lifecycle. A temporary flag without a removal plan can become a permanent addition to the codebase and increase its complexity over time.&lt;/p&gt;

&lt;p&gt;Closing that gap requires a per-type staleness threshold, a script that checks it on a schedule, and a removal procedure that treats deletion PRs as part of planned engineering work. This approach builds removal into the same process as creation, classifies flags by type when they are created, and uses automated staleness checks to identify flags that require attention. Together, these changes make flag removal part of the same engineering process as flag creation.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Make Feature Flag Cleanup Part of the Engineering Lifecycle&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Feature flags work best when removal is treated as part of the same &lt;a href="https://geekyants.com/blog/what-is-the-geekyants-agentic-development-life-cycle-how-adlc-changes-conventional-product-engineering" rel="noopener noreferrer"&gt;&lt;strong&gt;engineering lifecycle&lt;/strong&gt;&lt;/a&gt; as creation and rollout. Clear ownership, type-specific staleness checks, scheduled reviews, and focused removal PRs help teams prevent temporary controls from becoming permanent technical debt. For teams looking to strengthen these practices across deployment, automation, monitoring, and production operations, &lt;a href="https://geekyants.com/engineering/devops" rel="noopener noreferrer"&gt;&lt;strong&gt;GeekyAnts’ DevOps consulting services&lt;/strong&gt;&lt;/a&gt; provide support across the software delivery lifecycle.&lt;/p&gt;

</description>
      <category>techtalks</category>
      <category>devops</category>
      <category>softwaredevelopment</category>
      <category>featureflags</category>
    </item>
    <item>
      <title>The Bug That Doesn't Show Up in Code Review: Why Your Flutter Web App Reloads on Safari</title>
      <dc:creator>GeekyAnts India Pvt Ltd</dc:creator>
      <pubDate>Mon, 28 Sep 2026 06:46:46 +0000</pubDate>
      <link>https://dev.to/geekyants/the-bug-that-doesnt-show-up-in-code-review-why-your-flutter-web-app-reloads-on-safari-2elp</link>
      <guid>https://dev.to/geekyants/the-bug-that-doesnt-show-up-in-code-review-why-your-flutter-web-app-reloads-on-safari-2elp</guid>
      <description>&lt;p&gt;Demo example app*:* &lt;a href="https://github.com/manuindersekhon/flutter-image-memory-demo" rel="noopener noreferrer"&gt;&lt;strong&gt;github.com/manuindersekhon/flutter-image-memory-demo&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://geekyants.com/en-in/hire-ai-developers" rel="noopener noreferrer"&gt;&lt;strong&gt;AI-assisted teams&lt;/strong&gt;&lt;/a&gt; now ship correct code faster than ever. But this article is about a class of failure that is not in the code at all. It lives in the gap between what the code says and what the device actually does. And that gap is exactly where all the velocity we gained gets eaten back.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Bug Report That Made No Sense&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Some time back, one of our production apps had a leaderboard. A simple, scrollable list of 100+ players, each row with a name, a score, and a 50x50 profile picture. Nothing fancy.&lt;/p&gt;

&lt;p&gt;Then the bug reports started coming in.&lt;/p&gt;

&lt;p&gt;“The leaderboard page keeps refreshing on Safari.” “The app closed by itself on my iPhone while scrolling.” No error in the console. No crash log with a stack trace. Nothing reproducible on our development machines. The page would simply reload on Safari as if the user had pressed refresh, and on &lt;a href="https://geekyants.com/en-in/service/mobile-app/ios-app-development-services" rel="noopener noreferrer"&gt;&lt;strong&gt;iOS the app&lt;/strong&gt;&lt;/a&gt; would just disappear.&lt;/p&gt;

&lt;p&gt;Two different platforms, two different symptoms, and as it turned out, one single bug. One that no code review could have caught.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Code That Passed Review&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Here is the code at the centre of it. This is roughly what our leaderboard row looked like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ListTile(
  leading: CircleAvatar(
    radius: 25,
    backgroundImage: NetworkImage(user.avatarUrl),
  ),
  title: Text(user.name),
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Take a moment and review it. It compiles. It is an idiomatic Flutter, straight from the documentation. The analyzer is happy. It renders perfectly on the MacBook and in every demo. Whether a teammate wrote it or an &lt;a href="https://geekyants.com/en-in/ai/ai-agent-development-services" rel="noopener noreferrer"&gt;&lt;strong&gt;AI agent&lt;/strong&gt;&lt;/a&gt; generated it, any of us would have approved this diff.&lt;/p&gt;

&lt;p&gt;Nothing in the code is wrong. The defect only exists at the intersection of three layers that no code-level reviewer is looking at: &lt;em&gt;what the backend serves, what the image decoder does with it, and what the device does when memory runs out.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Layer One: What the URL Actually Returns&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Our users uploaded their profile pictures from their phones. A modern phone camera produces a 12-megapixel image, around 4032x3024 pixels, a few MB of JPEG. Our backend stored it exactly as uploaded, and the avatar URL served it exactly as stored.&lt;/p&gt;

&lt;p&gt;The UI contract says “50 pixel avatar”. The API contract says “whatever the user uploaded”. There is no type system, no lint rule, and no review checklist that connects these two, and that mismatch travels silently all the way to the user's device.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Layer Two: File Size Is Not Memory Size&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;This is the part of image handling that is easy to forget. A JPEG is a compressed format, but a screen cannot draw compressed bytes. Before anything is rendered, the image is decoded into a raw bitmap: four bytes for every pixel.&lt;/p&gt;

&lt;p&gt;So that 3 MB JPEG from the user's phone becomes 4032 x 3024 x 4 bytes, which is about 46.5 MB of memory. For one avatar. In a 50-pixel circle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And here is the key Flutter behaviour&lt;/strong&gt;: unless you tell it otherwise, &lt;em&gt;Flutter decodes an image at its full intrinsic size,&lt;/em&gt; not at the size of the widget displaying it. The CircleAvatar's 50-pixel constraint never reaches the decoder. The full bitmap is decoded, kept, and scaled down on every frame.&lt;/p&gt;

&lt;p&gt;To verify this, we built a small demo app that recreates the leaderboard and prints what the engine actually holds for each image. This is not theory, this is the app reporting its own image cache:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fymn3g0szwt2qgkrxq6k3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fymn3g0szwt2qgkrxq6k3.png" alt="Chrome demo leaderboard showing Flutter image memory usage with full-size avatar decoding" width="800" height="461"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The demo leaderboard in Chrome. The bar at the bottom is the app reporting its own decoded image memory: avatar #1 is held at 4032x3024, 46.51 MB, for a 50px circle.&lt;/p&gt;

&lt;p&gt;Multiply that by a leaderboard. Scrolling through 100+ entries asks the engine for several gigabytes of decoded bitmaps. Flutter's built-in image cache has a 100 MB budget, but that budget only applies to images that are no longer on screen. Images that are currently visible, or kept alive by the layout, are held regardless of it. The cap we were all silently relying on was never going to save us.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Layer Three: Every Platform Has a Ceiling, and They Are All Different&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Here is where the story splits by platform, and where “works on my machine” stops being a joke and becomes the actual mechanism of the bug.&lt;/p&gt;

&lt;p&gt;Chrome survives this abuse. It decodes images through a dedicated browser API into memory it can discard and re-decode at will. When we scrolled our naive leaderboard in Chrome, the tab stayed around a few hundred MB and nothing bad happened. This is exactly why the bug never appeared on our development machines.&lt;/p&gt;

&lt;p&gt;Safari has no such decode path. The decoded bitmaps land inside the tab's own web content process, and &lt;strong&gt;WebKit&lt;/strong&gt; enforces a hard memory budget on that process. On the same page, same scroll, Safari's process climbed to four times what Chrome used. Push further, and WebKit does exactly what its source code says it will do: &lt;em&gt;it kills the web content process and reloads the page.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In our stress test, the process crossed 7 GB and was terminated, and the page came back fresh, exactly the “random refresh” our users reported.&lt;/strong&gt; Safari even tells the user politely: “This webpage was reloaded because it was using significant memory.”&lt;/p&gt;

&lt;p&gt;There is also a subtler symptom on iPads and iPhones. Before killing the page, WebKit fights back by purging decoded images. We watched the iOS Safari process balloon, get purged, and keep running with the leaderboard text intact but every avatar blank:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fugfh70yb1fwuajb1i3d0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fugfh70yb1fwuajb1i3d0.png" alt="iOS Safari leaderboard after WebKit memory purge causes profile avatars to disappear" width="800" height="1739"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;iOS Safari after WebKit's memory purge: the page survives, but the avatars are silently gone.&lt;/p&gt;

&lt;p&gt;And the native iOS app? Same bug, different executioner. iOS enforces a per-app memory budget through a system called &lt;strong&gt;Jetsam&lt;/strong&gt;. The budget depends on the device, community measurements put it under 1 GB on older iPhones and around 2 GB on mid-range ones. A leaderboard holding a few dozen 46 MB bitmaps alive walks into that limit within a couple of screen-heights of scrolling. &lt;strong&gt;The app is killed by the OS, not by your code.&lt;/strong&gt; There is no Dart exception and no useful stack trace, and your crash reporting tool shows an “out of memory session” at best. &lt;em&gt;That was our mysterious iOS crash.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;One bug. Three ceilings. Three completely different symptoms.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Why Review Never Had a Chance&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Now go back to that CircleAvatar snippet and ask: where in the diff is this bug?&lt;/p&gt;

&lt;p&gt;It is not there. The reviewer sees idiomatic widget code. The tests see an image that renders. CI is green. The 46.5 MB number exists only at runtime, on a real device, with real production images. The defect is spread across three systems whose owners never appear in the same pull request: the upload pipeline that stores 12-megapixel photos, the framework default that decodes at intrinsic size, and the platform policy that kills the process.&lt;/p&gt;

&lt;p&gt;This is also why the AI angle matters to us. An agent will write you this exact code, and it will be right by every static standard. An AI reviewer will approve it for the same reason a human does: &lt;em&gt;the evidence is simply not in the artifact being reviewed.&lt;/em&gt; As AI compresses the cost of writing code, the defects that survive migrate to the layers that neither the agent nor the reviewer can see. The cost does not disappear. It moves to a production incident three weeks later, on a device you do not own.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Fix Is One Parameter (and a Better One Upstream)&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The immediate fix is embarrassingly small. Flutter lets you tell the decoder what size you actually need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;network&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;width:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;height:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;fit:&lt;/span&gt; &lt;span class="n"&gt;BoxFit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cover&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;cacheWidth:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;MediaQuery&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;devicePixelRatioOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// CircleAvatar has no sizing hook, so wrap the provider:&lt;/span&gt;
&lt;span class="n"&gt;CircleAvatar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;backgroundImage:&lt;/span&gt; &lt;span class="n"&gt;ResizeImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;NetworkImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nl"&gt;width:&lt;/span&gt; &lt;span class="mi"&gt;150&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 cacheWidth set, the same avatar decodes at 150 pixels instead of 4032. In our demo, that took each image from 46.51 MB down to 0.06 MB, roughly 775 times less memory, with zero visible difference in a 50-pixel circle. The whole leaderboard now fits in less than one megabyte of decoded images:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F39rfqeopi0aufllo80gx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F39rfqeopi0aufllo80gx.png" alt="Optimized leaderboard showing reduced image memory usage with efficient avatar caching" width="800" height="1739"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The fixed build: same leaderboard, same photos, 0.8 MB of decoded images in total. The probe shows avatar #1 now decodes at 150x112, 0.06 MB.&lt;/p&gt;

&lt;p&gt;But honestly, the client-side parameter is the band-aid. The &lt;strong&gt;real fix is to never ship a 12-megapixel file to a 50-pixel widget in the first place&lt;/strong&gt;: serve resized images from your CDN or an image proxy. That also fixes what cacheWidth cannot, because the full-size download and the browser-side decode of the original file still happen either way. Fix it at the source and every client, including the ones you have not written yet, gets it for free.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Making Sure It Never Comes Back&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;A bug that review cannot catch needs guardrails that do not depend on review.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The one we wish we had turned on earlier is &lt;a href="https://geekyants.com/en-in/hire-flutter-developers" rel="noopener noreferrer"&gt;&lt;strong&gt;built into Flutter&lt;/strong&gt;&lt;/a&gt; itself. Set debugInvertOversizedImages to true in your debug builds, and the framework will flip and invert the colours of any image that was decoded significantly larger than its display size, and log the wasted bytes. Our leaderboard lit up like this:&lt;/p&gt;

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

&lt;p&gt;debugInvertOversizedImages in action: the framework flags every avatar that was decoded far larger than the size it is displayed at.&lt;/p&gt;

&lt;p&gt;Beyond that flag, a few habits close the loop:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Wrap remote images in one shared widget&lt;/strong&gt; (an AppAvatar of your own) that requires a decode size, so the naive version cannot be written casually.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Test on the smallest-RAM device you actually support&lt;/strong&gt;, with production images, not neat little asset placeholders.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;When a “random refresh on Safari” or an “app just closed” report comes in, &lt;strong&gt;put memory on the suspect list before routing and state management.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;The most interesting thing about this bug is how ordinary the code was. No clever trick went wrong, no obscure API was misused. A default did exactly what it was documented to do, on inputs nobody in the pull request could see, on devices with limits nobody in the pull request was thinking about.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://geekyants.com/en-in/ai" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;em&gt;AI&lt;/em&gt;&lt;/strong&gt;&lt;/a&gt; &lt;em&gt;has made the code layer cheap. It has not made the other layers cheap.&lt;/em&gt; The asset pipeline, the decoder, the memory ceilings of a five-year-old iPhone, someone on the team still has to own those. The teams that move fastest with AI will not be the ones that generate the most code. They will be the ones who know exactly which questions the diff cannot answer, and go looking for the evidence themselves.&lt;/p&gt;

&lt;p&gt;The demo app used for every number and screenshot in this article is open source, and reproduces the whole story, including the Safari reload: &lt;a href="https://github.com/manuindersekhon/flutter-image-memory-demo" rel="noopener noreferrer"&gt;&lt;strong&gt;github.com/manuindersekhon/flutter-image-memory-demo&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>ai</category>
      <category>webdev</category>
      <category>code</category>
    </item>
    <item>
      <title>Why Legacy Systems Make Business Growth More Expensive: Navigate A Smarter Path to Legacy Modernization</title>
      <dc:creator>GeekyAnts India Pvt Ltd</dc:creator>
      <pubDate>Wed, 23 Sep 2026 11:53:55 +0000</pubDate>
      <link>https://dev.to/geekyants/why-legacy-systems-make-business-growth-more-expensive-navigate-a-smarter-path-to-legacy-57e</link>
      <guid>https://dev.to/geekyants/why-legacy-systems-make-business-growth-more-expensive-navigate-a-smarter-path-to-legacy-57e</guid>
      <description>&lt;p&gt;Learn how legacy systems make business growth more expensive and how edge-first modernization can remove constraints without replacing the existing system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Six Brands, Six Rebuilds, Until We Changed the Equation
&lt;/h2&gt;

&lt;p&gt;A North American restaurant group came to &lt;a href="https://geekyants.com/" rel="noopener noreferrer"&gt;GeekyAnts&lt;/a&gt; with six casual-dining brands and a digital estate. The system encountered challenges every time it needed to change as per the business requirements.&lt;/p&gt;

&lt;p&gt;There was an aging JSP web application, a separate native mobile app for each brand, and separate build and release pipelines for each of the systems. Each brand also had its own web and &lt;a href="https://geekyants.com/service/hire-mobile-app-development-services" rel="noopener noreferrer"&gt;mobile development teams&lt;/a&gt;. A change that should have been made once had to be implemented six times.&lt;/p&gt;

&lt;p&gt;Adding another brand took two to three months, and most of that time was going into creating another app, another pipeline, and duplicated work across the web and mobile teams. The core systems were not the issue since some of the routine tasks like ordering, menus, pricing, and availability were already working in a functional manner.&lt;/p&gt;

&lt;p&gt;The cost was actually spent in the existing digital layer around the system, where every additional brand meant another application, another pipeline, and more duplicated work. We started with a two-page &lt;a href="https://geekyants.com/blog/building-a-proof-of-concept-a-complete-guide-with-implementation-strategies" rel="noopener noreferrer"&gt;proof of concept&lt;/a&gt;. The question that needed to be answered was if the same components could genuinely be shared between web and native mobile. Once this assumption was tested, it gave us the basis to replace the edge while keeping the core systems in place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bringing Changes to One Digital Platform Instead of Adding Six New Rebuilds
&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiwd8dyvgkly71srxmxwv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiwd8dyvgkly71srxmxwv.png" alt="Digital platform modernization architecture with edge components replaced while core systems are retained" width="800" height="417"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We focused on the layer where the duplication was happening and built one platform for web and mobile, so that the changes could be made once instead of across six separate brand applications. A shared component library removed repeated development work, while a single mobile app shell let the team configure and roll out new brands instead of building another app from scratch. Over-the-air updates also meant changes could reach users without repeating the full app release cycle for every brand.&lt;/p&gt;

&lt;p&gt;The first brand went live in 6-8 months. After that, a new brand was onboarded in 2-3 weeks instead of 2-3 months. The per-brand teams were consolidated into one web team and one mobile team, freeing them from rebuilding the same screen and flow.&lt;/p&gt;

&lt;p&gt;The seventh brand was added after the platform existed, without recreating the old setup. The goal was to remove the multiplication that came with every new brand.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Does Legacy Infrastructure Become a Business Problem?
&lt;/h2&gt;

&lt;p&gt;Aging infrastructure alone is not the best reason to replace a system. In actuality, business constraints are one of the crucial reasons why legacy infrastructure needs to be replaced.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.mckinsey.com/capabilities/tech-and-ai/our-insights/demystifying-digital-dark-matter-a-new-standard-to-tame-technical-debt" rel="noopener noreferrer"&gt;McKinsey&lt;/a&gt; estimates that technical debt can account for 20-40% of the value of an organization’s technology estate. So when a system becomes a problem, the business starts paying for its limitations. A release that once took two weeks can stretch into a quarter. Adding a new brand, region, or product can mean adding another technology stack. An integration task that should be a simple process becomes a whole project if the system doesn’t connect with it the right way.&lt;/p&gt;

&lt;p&gt;The same pattern shows up in other business operations where a compliance change that should take weeks takes months, engineering teams spend more time keeping duplicated experiences running than building the next product, and each new unit of growth costs more as the business has to repeat the work it has already done.&lt;/p&gt;

&lt;p&gt;The better question ideally would be “What is this system preventing the business from doing, and what is that constraint costing us?” because it changes the modernization conversation from replacing old technology to removing the things that hold business growth back.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Did We Modernize Edge Before the Core System?
&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5zv8t2xq35czxn9gz9mg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5zv8t2xq35czxn9gz9mg.png" alt="Edge-first modernization diagram with before and after architecture with shared web and mobile components" width="800" height="683"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When a core system starts holding the business back, the first instinct would be to replace it, which may be a risky process.&lt;/p&gt;

&lt;p&gt;Most of the business processes may not be documented in one place, as some may exist in code, some in old integrations, and some may only be known by the people who have worked with the system for years.&lt;/p&gt;

&lt;p&gt;The edge is different. It is where customers, employees, and partners interact with the business: &lt;a href="https://geekyants.com/service/hire-web-app-development-services" rel="noopener noreferrer"&gt;web&lt;/a&gt; and mobile experiences, APIs, integrations, workflows, automation, and deployment pipelines.&lt;/p&gt;

&lt;p&gt;Modernizing the edge gives the businesses enough room to be adaptive to changes without immediately disrupting the logic on which the systems run. You can improve how customers place an order, connect a new channel, or streamline an employee workflow without interrupting the system that handles ordering or pricing tasks.&lt;/p&gt;

&lt;p&gt;The risk, however, changes as you move closer to the core system. The more business logic and dependencies you have to uncover, the more you risk finding rules nobody remembered were there.&lt;/p&gt;

&lt;p&gt;Needless to say, &lt;strong&gt;modernization risk increases with the amount of business logic you have to rediscover.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Seven Decisions To Be Considered for Modernization of Legacy Systems
&lt;/h2&gt;

&lt;p&gt;Every part of the legacy system can have a different path forward. Depending on what it does and where it creates friction, you can:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Retire&lt;/strong&gt; the system when nothing depends on it anymore.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Retain&lt;/strong&gt; when it works and creates no business constraint.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Expose&lt;/strong&gt; when the capability is useful but trapped behind poor interfaces.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Augment&lt;/strong&gt; when the core works but the surrounding experience does not.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Re-platform&lt;/strong&gt; when the application works but the underlying runtime has become the problem.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Refactor&lt;/strong&gt; when the business logic is worth keeping but the architecture prevents change or scale.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Replace&lt;/strong&gt; when the economics no longer make sense or the system's business logic no longer matches how the company operates.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Modernization Should Create Room for Business Growth
&lt;/h2&gt;

&lt;p&gt;The best place to begin the modernization process often starts from navigating which business process is actually hindering its growth. The real opportunity in &lt;a href="https://geekyants.com/enterprise-system-modernization" rel="noopener noreferrer"&gt;legacy modernization&lt;/a&gt; lies in making &lt;strong&gt;room for business’s growth without creating unnecessary risks.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Planning a legacy modernization initiative? Talk to GeekyAnts about identifying what is actually constraining growth and modernizing it without replacing the existing system.&lt;/p&gt;

</description>
      <category>legacycode</category>
      <category>webdev</category>
      <category>architecture</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>From Prompting to Process: What Changed When Flutter Shipped Agent Skills</title>
      <dc:creator>GeekyAnts India Pvt Ltd</dc:creator>
      <pubDate>Wed, 23 Sep 2026 10:52:52 +0000</pubDate>
      <link>https://dev.to/geekyants/from-prompting-to-process-what-changed-when-flutter-shipped-agent-skills-4jjk</link>
      <guid>https://dev.to/geekyants/from-prompting-to-process-what-changed-when-flutter-shipped-agent-skills-4jjk</guid>
      <description>&lt;p&gt;Google is starting to ship Flutter’s engineering workflows as machine-readable guidance for &lt;a href="https://geekyants.com/ai/ai-agent-development-services" rel="noopener noreferrer"&gt;AI agents&lt;/a&gt;. It may look like another AI feature, but it hints at a much bigger shift in how teams build with &lt;a href="https://geekyants.com/hire-flutter-developers" rel="noopener noreferrer"&gt;Flutter&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Consistency Problem
&lt;/h2&gt;

&lt;p&gt;Over the past few years, &lt;a href="https://geekyants.com/blog/how-is-ai-making-software-development-easier" rel="noopener noreferrer"&gt;AI coding tools&lt;/a&gt; like &lt;a href="https://geekyants.com/blog/cursor-vs-lovable-vs-replit-which-vibe-coding-tool-builds-the-most-production-ready-code" rel="noopener noreferrer"&gt;Cursor&lt;/a&gt;, Claude Code, Copilot, and OpenCode have become part of many developers’ daily workflows. An AI coding tool can produce code, explain unfamiliar APIs, write tests, and navigate large codebases with impressive accuracy.&lt;/p&gt;

&lt;p&gt;That capability now stretches across stacks and workflows, from Node.js, TypeScript, and React to Python projects. It can help with debugging, boilerplate automation, integrations with services such as Stripe’s API, and work involving MongoDB or AWS tools.&lt;/p&gt;

&lt;p&gt;But if you have been using these tools on a reasonably large project, you have probably noticed something.&lt;/p&gt;

&lt;p&gt;They are not very consistent.&lt;/p&gt;

&lt;p&gt;Ask an AI to implement the same feature in two different sessions, and there’s a good chance you will get two different approaches. Switch models, and the implementation changes again. Sometimes it follows the latest framework recommendations. Other times, it relies on outdated patterns. The same problem can appear whether the agent is changing a Flutter feature, a React app, a fintech prototype, or larger Enterprise apps.&lt;/p&gt;

&lt;p&gt;Most teams respond the same way: write better prompts, add repository rules, or create custom &lt;a href="https://agentskills.io/home" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;em&gt;skill.md&lt;/em&gt;&lt;/strong&gt;&lt;/a&gt; files to steer the agent toward the right decisions.&lt;/p&gt;

&lt;p&gt;At GeekyAnts, we took the same approach by documenting our architecture, coding conventions, review expectations, and engineering practices as custom Skills. They made the agent noticeably more consistent.&lt;/p&gt;

&lt;p&gt;So when Flutter announced official Agent Skills, my first reaction was not: &lt;em&gt;“How do we use them?”&lt;/em&gt; It was: &lt;em&gt;“If we already have our own Skills, what problem are Flutter's official Skills actually solving?”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That question turned into a couple of experiments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three Layers of AI-Assisted Development
&lt;/h2&gt;

&lt;p&gt;Looking back, Flutter's recent AI investments weren't isolated features. They were building on each other.&lt;/p&gt;

&lt;p&gt;First came &lt;strong&gt;Rules&lt;/strong&gt;, giving teams a way to define project-specific conventions and preferences.&lt;/p&gt;

&lt;p&gt;Then came &lt;a href="https://geekyants.com/blog/mcp-in-action-a-developers-take-on-smarter-service-coordination" rel="noopener noreferrer"&gt;&lt;strong&gt;Model Context Protocol (MCP)&lt;/strong&gt;&lt;/a&gt;, allowing AI agents to inspect, debug, and interact with running Flutter applications instead of reasoning purely from static code.&lt;/p&gt;

&lt;p&gt;And then came &lt;strong&gt;Agent Skills.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If Rules tell an agent &lt;strong&gt;how your team works&lt;/strong&gt;, and MCP tells it &lt;strong&gt;what’s happening inside your application&lt;/strong&gt;, Agent Skills answer a different question: &lt;strong&gt;how does Flutter itself recommend solving this problem?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That is the important shift.&lt;/p&gt;

&lt;p&gt;Flutter is now versioning its engineering workflows alongside the framework itself. Instead of relying entirely on what an &lt;a href="https://geekyants.com/ai/ai-development-services" rel="noopener noreferrer"&gt;AI model&lt;/a&gt; happened to learn during training, agents can follow workflows maintained by the Flutter team.&lt;/p&gt;

&lt;p&gt;Today, those workflows cover areas like:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Localization&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Responsive layouts&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Routing&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Widget and unit testing&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Static analysis&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;JSON serialization&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Platform integration&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Architecture best practices&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In other words, Flutter is shipping its engineering knowledge as structured workflows.&lt;/p&gt;

&lt;p&gt;That naturally led to the next question: &lt;strong&gt;does this actually change how an AI agent behaves?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I ran two experiments to find out.&lt;/p&gt;

&lt;h2&gt;
  
  
  First Experiment: Declarative Routing
&lt;/h2&gt;

&lt;p&gt;It is one of those features where there is not just one thing to do. Depending on the prompt, an AI could jump straight into writing routes, miss platform-specific configuration, skip deep linking altogether, or recommend an approach based on what it learned during training rather than Flutter's latest guidance.&lt;/p&gt;

&lt;p&gt;So I kept the prompt intentionally simple:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;“Set up declarative routing for this Flutter application.”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I wanted to see how the agent would approach the problem before I told it how to solve it.&lt;/p&gt;

&lt;p&gt;The first thing it did caught my attention.&lt;/p&gt;

&lt;p&gt;Before generating an implementation plan, it explicitly selected the &lt;strong&gt;&lt;em&gt;flutter-setup-declarative-routing&lt;/em&gt;&lt;/strong&gt; skill.&lt;/p&gt;

&lt;p&gt;Flutter Agent Skill Selection for Declarative Routing&lt;/p&gt;

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

&lt;p&gt;From that point on, it was not about figuring out a solution; it was following Flutter's own workflow.&lt;/p&gt;

&lt;p&gt;That was the interesting part.&lt;/p&gt;

&lt;p&gt;Without Agent Skills, the implementation depends on the model’s reasoning and whatever Flutter knowledge it has internalized. With Agent Skills, the framework itself becomes the starting point.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens when Flutter's Skills and our own Skills are both available?
&lt;/h2&gt;

&lt;p&gt;I tested this with a login screen.&lt;/p&gt;

&lt;p&gt;It needed:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Flutter's localization workflow&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Our project conventions for authentication, repositories, dependency injection, and state management&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I kept the prompt simple and let the agent decide how to approach it:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;“Implement a login screen for this application. Follow the existing project architecture and add localization for all user-facing strings.”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It picked the right source every time.&lt;/p&gt;

&lt;p&gt;For localization, it used Flutter’s official Skill. For everything related to our application architecture, it followed our custom Skills. I did not have to tell it which one to use or write a carefully engineered prompt.&lt;/p&gt;

&lt;p&gt;The two sets of Skills worked together naturally. Flutter handled the framework guidance, while our repository continued to define how our application was built.&lt;/p&gt;

&lt;p&gt;Flutter Localization Workflow Using Agent Skills&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Why This Matters: A Floor, Not a Finish Line
&lt;/h2&gt;

&lt;p&gt;So, what actually changed?&lt;/p&gt;

&lt;p&gt;Flutter is taking ownership of its engineering expertise.&lt;/p&gt;

&lt;p&gt;Engineering teams no longer need to teach AI how Flutter expects routing, localization, responsive layouts, or testing to be implemented. Flutter now ships that knowledge itself.&lt;/p&gt;

&lt;p&gt;That removes a significant amount of duplication. Instead of every team maintaining its own version of Flutter best practices inside prompts or custom Skills, the official workflows become the source of truth.&lt;/p&gt;

&lt;p&gt;Even better, those workflows evolve alongside Flutter. As recommendations change, the official Skills change too, and every compatible AI agent benefits automatically.&lt;/p&gt;

&lt;p&gt;Of course, that’s only half the story.&lt;/p&gt;

&lt;p&gt;Flutter’s Agent Skills provide a strong foundation, but they do not replace your organization’s architecture, coding standards, or business-specific workflows. Those remain your responsibility, and that is exactly how it should be.&lt;/p&gt;

&lt;p&gt;This distinction matters across software development, whether teams are building for the web, mobile, or domains such as cybersecurity. Framework guidance can standardize common implementation patterns, while teams still need to define the context that is specific to their products and systems.&lt;/p&gt;

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

&lt;p&gt;Looking back, Flutter’s recent AI features fit together surprisingly well.&lt;/p&gt;

&lt;p&gt;Rules capture how &lt;strong&gt;your team&lt;/strong&gt; works. MCP gives agents &lt;strong&gt;runtime context&lt;/strong&gt;. Agent Skills teach them &lt;strong&gt;how Flutter itself expects problems to be solved&lt;/strong&gt;. Custom Skills layer on everything unique to your organization.&lt;/p&gt;

&lt;p&gt;Together, they reduce the amount of engineering knowledge an AI has to infer. That is the real significance of Flutter’s recent AI investments. Engineering knowledge is becoming explicit, versioned, reusable, and maintained by the people best positioned to own it.&lt;/p&gt;

&lt;p&gt;The same idea is becoming increasingly relevant as teams work with LLMs and AI-driven automation. The more implementation knowledge can be made explicit and reusable, the less an agent has to guess from prompts or training data alone.&lt;/p&gt;

&lt;p&gt;Today’s skill library covers foundational workflows, but it already hints at what’s possible. Imagine Skills for performance profiling, DevTools workflows, accessibility audits, plugin development, migrations, or advanced rendering patterns. Every new skill moves another piece of framework knowledge out of documentation and into a reusable workflow.&lt;/p&gt;

&lt;p&gt;The answer to &lt;strong&gt;how much of Flutter an agent should really guess keeps&lt;/strong&gt; shrinking with every release.&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>ai</category>
      <category>agentskills</category>
    </item>
    <item>
      <title>Building a Production-Ready Canva-like Editor with Konva.js, React 19 and Next.js 15</title>
      <dc:creator>GeekyAnts India Pvt Ltd</dc:creator>
      <pubDate>Thu, 10 Sep 2026 10:31:18 +0000</pubDate>
      <link>https://dev.to/geekyants/building-a-production-ready-canva-like-editor-with-konvajs-react-19-and-nextjs-15-3imd</link>
      <guid>https://dev.to/geekyants/building-a-production-ready-canva-like-editor-with-konvajs-react-19-and-nextjs-15-3imd</guid>
      <description>&lt;p&gt;This blog explains how to build a production-ready canvas editor with&lt;br&gt;
Konva.js, React, and Next.js, covering architecture, performance, and&lt;br&gt;
key engineering decisions.&lt;/p&gt;

&lt;p&gt;Author: Priyanka Rokhade, Software Engineer III&lt;br&gt;
Subject Matter Expert: Deepanshu Goyal, Senior Software Engineer -&lt;br&gt;
III&lt;/p&gt;

&lt;p&gt;Executive Summary: Why Build an In-App Canvas Editor?&lt;/p&gt;

&lt;p&gt;Modern SaaS&lt;br&gt;
applications&lt;br&gt;
increasingly require users to create visually rich documents directly&lt;br&gt;
inside the browser. Whether it is travel itineraries, reports,&lt;br&gt;
certificates, brochures, or marketing collateral, users expect the same&lt;br&gt;
drag-and-drop experience offered by tools like Canva---but without&lt;br&gt;
leaving the application.&lt;/p&gt;

&lt;p&gt;Our challenge was straightforward:&lt;/p&gt;

&lt;p&gt;"How do we build a Canva-like editor that feels native, performs&lt;br&gt;
smoothly, and integrates seamlessly with our product?"&lt;/p&gt;

&lt;p&gt;After evaluating multiple approaches---including embedded design&lt;br&gt;
tools,&lt;br&gt;
HTML-based editors, and a fully custom canvas engine---we built our&lt;br&gt;
editor on Konva.js + React-Konva.&lt;/p&gt;

&lt;p&gt;The result was a production-ready editor capable of:&lt;/p&gt;

&lt;p&gt;60 FPS interaction&lt;/p&gt;

&lt;p&gt;250+ canvas objects&lt;/p&gt;

&lt;p&gt;Rich text editing&lt;/p&gt;

&lt;p&gt;Autosave&lt;/p&gt;

&lt;p&gt;Multi-page documents&lt;/p&gt;

&lt;p&gt;Responsive previews&lt;/p&gt;

&lt;p&gt;Pixel-perfect rendering between editor and viewer&lt;/p&gt;

&lt;p&gt;This article presents the architecture, design&lt;br&gt;
decisions,&lt;br&gt;
production challenges, and engineering lessons behind building a&lt;br&gt;
production-ready canvas editor.&lt;/p&gt;

&lt;p&gt;Business Objectives&lt;/p&gt;

&lt;p&gt;Beyond replicating Canva-like functionality, the primary objective was&lt;br&gt;
to eliminate dependence on external design tools and bring document&lt;br&gt;
creation directly into our platform. By integrating editing, previewing,&lt;br&gt;
and publishing into a single workflow, the editor reduces operational&lt;br&gt;
overhead, shortens content turnaround time, and enables teams to create&lt;br&gt;
production-ready documents without switching between multiple&lt;br&gt;
applications. This also gives the product team complete control over the&lt;br&gt;
editing experience, data ownership, and future feature development.&lt;/p&gt;

&lt;p&gt;Why We Chose Konva.js Over Other Alternatives&lt;/p&gt;

&lt;p&gt;When we started designing the editor, we evaluated three possible&lt;br&gt;
approaches.&lt;/p&gt;

&lt;p&gt;At first glance, embedding a tool such as Canva or Figma looked&lt;br&gt;
attractive because this approach reduced implementation effort. However,&lt;br&gt;
licensing costs, limited customization, and data ownership concerns&lt;br&gt;
quickly ruled it out.&lt;/p&gt;

&lt;p&gt;Next, we experimented with HTML-based editors built using absolutely&lt;br&gt;
positioned &lt;/p&gt; elements. Although this worked for simple layouts,&lt;br&gt;
performance degraded significantly as documents became more complex.

&lt;p&gt;Ultimately, we chose Konva.js because it provided a scene graph&lt;br&gt;
architecture, high-performance rendering, and complete control over the&lt;br&gt;
editing experience.&lt;/p&gt;

&lt;p&gt;When evaluating how to build this visual editor, we assessed three&lt;br&gt;
architectural paths:&lt;/p&gt;

&lt;p&gt;Architectural Approach  How It Works            Why It Succeeded or&lt;br&gt;
Failed in Production&lt;/p&gt;

&lt;p&gt;Third-Party Embeds      Embeds an external      Failed: High recurring&lt;br&gt;
(e.g.&amp;nbsp;Canva / Figma SDK design tool inside our  per-user licensing&lt;br&gt;
via iFrame)             web page using an       fees; user data lives&lt;br&gt;
iFrame.                 on external servers;&lt;br&gt;
inability to build&lt;br&gt;
custom domain features&lt;br&gt;
such as custom torn&lt;br&gt;
image frames, Unsplash&lt;br&gt;
search panel, and&lt;br&gt;
specific Google Font&lt;br&gt;
pickers.&lt;/p&gt;

&lt;p&gt;HTML/DOM-Based Editors  Renders elements as     Failed: When a document&lt;br&gt;
(e.g.&amp;nbsp;GrapesJS /        standard HTML &lt;/p&gt;   contains 50+ elements&lt;br&gt;
Absolute CSS Divs)      tags positioned with    with rotations, drop&lt;br&gt;
CSS.                    shadows, and masks, DOM&lt;br&gt;
repaints cause&lt;br&gt;
noticeable lag during&lt;br&gt;
dragging. Rotation and&lt;br&gt;
corner resize handle&lt;br&gt;
math also glitch across&lt;br&gt;
different web browsers.

&lt;p&gt;Practical Benefits of Our Konva.js Architecture&lt;/p&gt;

&lt;p&gt;100% Visual Fidelity (Zero Rendering Drift): Both the admin&lt;br&gt;
design editor and the public viewer application use the exact same&lt;br&gt;
Konva shape primitives (Konva.Text, Konva.Image, Konva.Rect). What&lt;br&gt;
the creator designs on their screen is 100% identical to what&lt;br&gt;
end-users see---no displaced text, shifting margins, or&lt;br&gt;
browser-specific rendering bugs.&lt;/p&gt;

&lt;p&gt;Lightweight Universal Canvas Format (UCF JSON): Instead of&lt;br&gt;
saving heavy image files or fragile HTML, our editor serializes&lt;br&gt;
document pages into clean, portable JSON, including item&lt;br&gt;
coordinates, font size, and fill colors. A complete 10-page document&lt;br&gt;
is under 15 KB, loads instantly, and is stored securely in our cloud&lt;br&gt;
database and object storage.&lt;/p&gt;

&lt;p&gt;Production Impact: Beyond the technical architecture, the editor&lt;br&gt;
delivered measurable improvements to our internal workflow: reduced&lt;br&gt;
document creation time from 1--2 days to under 15 minutes by&lt;br&gt;
eliminating external design tools; supports 250+ canvas objects&lt;br&gt;
while maintaining smooth 60 FPS interactions; replaced fragmented&lt;br&gt;
designer-to-operations workflows with a fully integrated in-app&lt;br&gt;
editing experience; and enabled creators to design, preview, and&lt;br&gt;
publish documents without leaving the platform.&lt;/p&gt;

&lt;p&gt;Customer Value: Enables operations teams to publish customer&lt;br&gt;
documents 95% faster. Eliminates dependence on external design&lt;br&gt;
tools. Keeps customer data inside the platform. Reduces onboarding&lt;br&gt;
time for non-design users.&lt;/p&gt;

&lt;p&gt;What the Editor Does&lt;/p&gt;

&lt;p&gt;The editor operates inside the web&lt;br&gt;
application&lt;br&gt;
workspace and enables users to:&lt;/p&gt;

&lt;p&gt;Compose multi-page visual documents featuring text, vector shapes,&lt;br&gt;
high-resolution photography, video clips, buttons, and hyperlinks.&lt;/p&gt;

&lt;p&gt;Drag, resize, rotate, and layer elements with pixel-level precision&lt;br&gt;
on an interactive 2D canvas.&lt;/p&gt;

&lt;p&gt;Apply custom Google Fonts, decorative frames (torn edge, square&lt;br&gt;
borders), mask clippings (circle, star, heart, diamond), image&lt;br&gt;
cropping, and character-level rich text formatting.&lt;/p&gt;

&lt;p&gt;Preview responsive layouts in real-time across web and mobile&lt;br&gt;
device&lt;br&gt;
viewports.&lt;/p&gt;

&lt;p&gt;Autosave design state with debouncing and publish completed&lt;br&gt;
documents directly to the client viewing application.&lt;/p&gt;

&lt;p&gt;Primary users: The editor is designed for internal operations teams,&lt;br&gt;
content creators, and administrators responsible for producing&lt;br&gt;
customer-facing documents. Instead of relying on external design&lt;br&gt;
software, users can create, review, and publish visual content directly&lt;br&gt;
within the application, reducing context switching and simplifying&lt;br&gt;
day-to-day workflows.&lt;/p&gt;

&lt;p&gt;Application scope: Integrated visual design module within the Admin&lt;br&gt;
Web Workspace.&lt;/p&gt;

&lt;p&gt;Why Konva?&lt;/p&gt;

&lt;p&gt;Konva provides decisive technical advantages for our production&lt;br&gt;
requirements:&lt;/p&gt;

&lt;p&gt;Scene Graph Hierarchy: A clean Stage → Layer → Group → Shape&lt;br&gt;
tree that maps 1:1 to document pages and layered canvas items.&lt;/p&gt;

&lt;p&gt;Built-in Drag, Transform &amp;amp; Hit Detection: Accelerated&lt;br&gt;
mathematical routines for drag-and-drop, multi-node rotation, corner&lt;br&gt;
scaling, and pointer hit detection.&lt;/p&gt;

&lt;p&gt;Interactive Transformer: Customizable bounding box with 8 anchor&lt;br&gt;
handles, rotation anchor, and aspect-ratio constraints out of the&lt;br&gt;
box.&lt;/p&gt;

&lt;p&gt;Declarative React Bindings: Allows canvas elements to be&lt;br&gt;
composed declaratively with standard React props, state hooks, and&lt;br&gt;
component lifecycles.&lt;/p&gt;

&lt;p&gt;Universal Canvas Format Serialization: Rather than storing the&lt;br&gt;
document as an image, we store every object as JSON. Each element&lt;br&gt;
records information such as position, size, color, font, rotation,&lt;br&gt;
and opacity. This lightweight format allows us to recreate the exact&lt;br&gt;
same document anywhere using Konva.&lt;/p&gt;

&lt;p&gt;Konva Fundamentals&lt;/p&gt;

&lt;p&gt;For developers exploring Konva, four foundational primitives form the&lt;br&gt;
foundation of our canvas architecture:&lt;/p&gt;

&lt;p&gt;Konva Concept           Core Responsibility     Implementation in Our&lt;br&gt;
Editor&lt;/p&gt;

&lt;p&gt;Stage                   The root canvas         One Konva Stage per&lt;br&gt;
container managing      document page inside&lt;br&gt;
global dimensions,      our canvas container.&lt;br&gt;
viewport scaling, and&lt;br&gt;
top-level mouse/touch&lt;br&gt;
events.&lt;/p&gt;

&lt;p&gt;Layer                   An independent HTML5 2D Three discrete layers:&lt;br&gt;
canvas drawing surface  Background layer,&lt;br&gt;
with isolated redraw    elements layer, and&lt;br&gt;
loops.                  transformer/UI overlay&lt;br&gt;
layer.&lt;/p&gt;

&lt;p&gt;Shape                   Drawable nodes on the   One Konva shape per&lt;br&gt;
canvas (Text, Rect,     document element&lt;br&gt;
Circle, Line, Arrow,    dispatched dynamically&lt;br&gt;
Image, Star, etc.).     via our shape rendering&lt;br&gt;
engine.&lt;/p&gt;

&lt;p&gt;Shape Registration: All required Konva shapes are registered at app&lt;br&gt;
initialization---including Rect, Circle, Ellipse, Text, Image, Line,&lt;br&gt;
Arrow, RegularPolygon, Star, Wedge, and Arc---ensuring tree-shaking&lt;br&gt;
keeps bundle size minimal while guaranteeing all element types render&lt;br&gt;
without runtime errors.&lt;/p&gt;

&lt;p&gt;Editor Architecture at a Glance&lt;/p&gt;

&lt;p&gt;The editor is engineered as a hybrid Next.js/React application wrapped&lt;br&gt;
around a high-performance Konva canvas. React governs the outer UI&lt;br&gt;
chrome, toolbar actions, sidebar panels, and state management, while&lt;br&gt;
Konva drives the 2D visual layout surface.&lt;/p&gt;

&lt;p&gt;Figure: High-Level Architecture: React UI Chrome, State Layer, Canvas&lt;br&gt;
Engine, and Output Pipeline&lt;/p&gt;

&lt;p&gt;The Hybrid Canvas Model&lt;/p&gt;

&lt;p&gt;One of the biggest engineering decisions was not using the canvas for&lt;br&gt;
everything. At first, we tried rendering every interaction directly&lt;br&gt;
inside Konva. It quickly became obvious that some browser features&lt;br&gt;
simply work better in the DOM.&lt;/p&gt;

&lt;p&gt;Examples include:&lt;/p&gt;

&lt;p&gt;Blinking text cursor&lt;/p&gt;

&lt;p&gt;Spell check&lt;/p&gt;

&lt;p&gt;Video controls&lt;/p&gt;

&lt;p&gt;Copy/paste&lt;/p&gt;

&lt;p&gt;Text selection&lt;/p&gt;

&lt;p&gt;Instead of fighting the browser, we built a Hybrid Canvas Architecture&lt;br&gt;
where Konva renders graphics while temporary HTML overlays handle&lt;br&gt;
editing.&lt;/p&gt;

&lt;p&gt;To combine the performance of canvas with the rich UX of the DOM, our&lt;br&gt;
editor implements a Hybrid Canvas Architecture:&lt;/p&gt;

&lt;p&gt;Figure: The Hybrid Canvas Architecture: Synchronized Konva Canvas and&lt;br&gt;
HTML DOM Overlays&lt;/p&gt;

&lt;p&gt;Why the Hybrid Model Matters&lt;/p&gt;

&lt;p&gt;Inline Text Editing: When a user double-clicks a text item, an&lt;br&gt;
invisible HTML  is mounted at the exact bounding box and&amp;lt;br&amp;gt;
rotation of the Konva text node---providing native cursor blinking,&amp;lt;br&amp;gt;
typing, and keyboard shortcuts.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Rich Text Formatting: Multi-range formatted text (bold, italic,&amp;lt;br&amp;gt;
underline per character slice) is painted directly onto the canvas&amp;lt;br&amp;gt;
via a custom sceneFunc (drawFormattedTextOnCanvas)---ensuring&amp;lt;br&amp;gt;
correct z-ordering without persistent DOM elements.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Video Playback: Video items display a poster thumbnail on&amp;lt;br&amp;gt;
canvas, while interactive playback, trimming, and audio controls&amp;lt;br&amp;gt;
appear in a synchronized DOM overlay.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Real-Time Overlay Synchronization: Floating toolbars and editing&amp;lt;br&amp;gt;
inputs continuously recalculate their CSS transforms during canvas&amp;lt;br&amp;gt;
panning, zooming, and item dragging.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Architectural Takeaway: By keeping DOM overlays transient (active&amp;lt;br&amp;gt;
only during direct editing) and painting all normal elements inside&amp;lt;br&amp;gt;
Konva, we preserve 60 FPS canvas performance while giving users full&amp;lt;br&amp;gt;
browser editing ergonomics.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;How a User Action Becomes Canvas State&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Every user interaction follows a strict unidirectional loop:&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;UI event → Global Editor State → Konva re-render → history push →&amp;lt;br&amp;gt;
debounced autosave&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Interaction Loop Steps&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;User Triggers Action: User clicks "Add heading" in the sidebar&amp;lt;br&amp;gt;
or drags an element on canvas.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Context Mutation: The action invokes addItem() or&amp;lt;br&amp;gt;
updateItem() in the global editor state.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;History Recording: The history manager pushes the previous&amp;lt;br&amp;gt;
snapshot onto the 50-state undo stack.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Canvas Re-draw: React-Konva receives updated props and&amp;lt;br&amp;gt;
re-renders the modified shapes on the elements layer.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Debounced Serialization: The autosave pipeline serializes canvas&amp;lt;br&amp;gt;
items to JSON and dispatches a debounced (2-second) PATCH request to&amp;lt;br&amp;gt;
the backend API.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Key User Flows&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Flow 1 --- Adding and Editing Text&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Figure: Flow 1: Adding, Rendering, and Inline-Editing Text Elements&amp;lt;br&amp;gt;
(Vertical Workflow)&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Konva Touchpoints: Konva.Text node, custom sceneFunc for formatted&amp;lt;br&amp;gt;
character ranges, and Transformer with scale-to-fontSize baking (scaling&amp;lt;br&amp;gt;
corner anchors adjusts fontSize directly to avoid pixelated text).&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Flow 2 --- Adding an Image from Unsplash&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Figure: Flow 2: Searching, Loading, and Rendering Unsplash Images&amp;lt;br&amp;gt;
(Vertical Workflow)&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Konva Touchpoints: Konva.Image node with HTMLImageElement source;&amp;lt;br&amp;gt;
mask clipping via custom clipFunc; aspect ratio preservation during&amp;lt;br&amp;gt;
transform handles.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Flow 3 --- Selection, Transform, and Snap&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Figure: Flow 3: Single/Multi-Selection, Transformer Attachment, and&amp;lt;br&amp;gt;
Snap Grid Guides&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Konva Touchpoints: Canvas Transformer with 8 anchor handles,&amp;lt;br&amp;gt;
real-time snap grid logic calculating alignment guidelines against&amp;lt;br&amp;gt;
canvas edges and sibling elements; arrows bypass Transformer and use&amp;lt;br&amp;gt;
2-point anchor handles.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Flow 4 --- Save, Preview, and Publish&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Figure: Flow 4: Autosave, UCF Serialization, Live Preview, and&amp;lt;br&amp;gt;
Production Publish&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Konva Touchpoints: Serialization transforms page scenes into&amp;lt;br&amp;gt;
Universal Canvas Format (UCF) JSON. The same Konva shape vocabulary is&amp;lt;br&amp;gt;
reused in the client viewer for 100% visual fidelity between editor&amp;lt;br&amp;gt;
preview and production viewer.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Supported Element Types&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;The editor supports 12 distinct element types, each mapped to a Konva&amp;lt;br&amp;gt;
primitive or custom renderer:&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Element Type            Konva / Custom Renderer Technical Implementation&amp;lt;br&amp;gt;
Notes&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Text                    Konva.Text + custom     Inline HTML textarea&amp;lt;br&amp;gt;
sceneFunc               editing; rich formatted&amp;lt;br&amp;gt;
character ranges&amp;lt;br&amp;gt;
(bold/italic/underline)&amp;lt;br&amp;gt;
painted on canvas.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Rectangle               Konva.Rect              Solid and gradient fills,&amp;lt;br&amp;gt;
border strokes,&amp;lt;br&amp;gt;
customizable corner&amp;lt;br&amp;gt;
radius, opacity.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Circle / Ellipse        Konva.Circle /          Uniform and non-uniform&amp;lt;br&amp;gt;
Konva.Ellipse           radial scaling with&amp;lt;br&amp;gt;
aspect lock support.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Line                    Konva.Line              Point coordinate array&amp;lt;br&amp;gt;
scaling and rotation&amp;lt;br&amp;gt;
handling during&amp;lt;br&amp;gt;
transform.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Arrow                   Konva.Arrow             Custom 2-point anchor&amp;lt;br&amp;gt;
editing (head and tail&amp;lt;br&amp;gt;
moved independently).&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Polygon / Star          Konva.RegularPolygon /  Configurable vertex&amp;lt;br&amp;gt;
Konva.Star              count, inner/outer radius&amp;lt;br&amp;gt;
ratio.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Wedge / Arc             Konva.Wedge / Konva.Arc Custom selection overlay&amp;lt;br&amp;gt;
with start/end angle&amp;lt;br&amp;gt;
dragging.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Image                   Konva.Image             Crop rectangle math,&amp;lt;br&amp;gt;
shape masks (circle,&amp;lt;br&amp;gt;
star, heart, diamond),&amp;lt;br&amp;gt;
opacity, filters.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Video                   Konva.Image frame +     Video poster on canvas;&amp;lt;br&amp;gt;
HTML overlay            synchronized DOM player&amp;lt;br&amp;gt;
(max 3 videos per&amp;lt;br&amp;gt;
document).&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Button / Link           Custom Group (Rect +    Clickable interactive&amp;lt;br&amp;gt;
Text)                   hotspot, URL navigation,&amp;lt;br&amp;gt;
document action binding.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Frame                   SquareFrameRenderer /   Decorative organic image&amp;lt;br&amp;gt;
TornFrameRenderer       container with clipping&amp;lt;br&amp;gt;
masks.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Dispatch logic operates using a clean TypeScript discriminated union&amp;lt;br&amp;gt;
(CanvasItem).&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;What Worked Well &amp;amp; Architectural Strengths&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Dev--Prod Parity for Rendering&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Designs export to UCF JSON and render in the client viewing app with the&amp;lt;br&amp;gt;
identical Konva primitives. Creators see in preview exactly what&amp;lt;br&amp;gt;
end-users experience---zero rendering drift or font mismatches.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Hook-Based Interaction Logic&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Complex canvas behaviors are decomposed into dedicated, testable custom&amp;lt;br&amp;gt;
React hooks rather than one monolithic component:&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Custom React Hook                   Core Responsibility&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;useDragHandlers                   Single-item drag, multi-selection&amp;lt;br&amp;gt;
drag, and transformer drag&amp;lt;br&amp;gt;
coordination.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;useTransformHandlers              Resize, rotate, scale commit per&amp;lt;br&amp;gt;
item type with aspect ratio&amp;lt;br&amp;gt;
constraints.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;useSelectionHandlers              Single click, shift/cmd&amp;lt;br&amp;gt;
multi-select, background click&amp;lt;br&amp;gt;
deselect.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;useTextEditing                    Double-click text editing&amp;lt;br&amp;gt;
activation, textarea placement,&amp;lt;br&amp;gt;
keyboard commit.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;useArrowHandlers                  Two-point arrow anchor handle&amp;lt;br&amp;gt;
dragging and coordinate&amp;lt;br&amp;gt;
calculation.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;useSnapGridLines                  Real-time alignment guide&amp;lt;br&amp;gt;
calculation and snapping against&amp;lt;br&amp;gt;
canvas &amp;amp; elements.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;useCanvasEffects                  Transformer attachment lifecycle,&amp;lt;br&amp;gt;
keyboard nudge handling (arrow&amp;lt;br&amp;gt;
keys).&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;useHistory                        50-state undo/redo stack with state&amp;lt;br&amp;gt;
compression and debounced push.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;useAutosave                       Debounced 2-second canvas&amp;lt;br&amp;gt;
serialization and PATCH API save&amp;lt;br&amp;gt;
pipeline.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;This modular structure keeps canvas orchestration clean, readable, and&amp;lt;br&amp;gt;
maintainable.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Production Performance Benchmarks &amp;amp; Metrics&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;To maintain smooth interactions on resource-constrained client machines,&amp;lt;br&amp;gt;
the canvas engine underwent rigorous benchmarking:&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Performance Dimension   Production Metric       Engineering Mechanism&amp;lt;br&amp;gt;
Achieved&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Interaction Frame Rate  Solid 60 FPS across     Node ref mutations&amp;lt;br&amp;gt;
250+ canvas elements    bypass React virtual&amp;lt;br&amp;gt;
DOM during active&amp;lt;br&amp;gt;
dragging and transform&amp;lt;br&amp;gt;
cycles.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Transformer Rotation    &amp;lt; 12 ms per frame      Layer splitting:&amp;lt;br&amp;gt;
Latency                 redraw cycle            transformer anchors&amp;lt;br&amp;gt;
render on an isolated&amp;lt;br&amp;gt;
canvas layer without&amp;lt;br&amp;gt;
invalidating elements.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Autosave Network        94% reduction in API    2-second debounce timer&amp;lt;br&amp;gt;
Reduction               write volume            on state mutations;&amp;lt;br&amp;gt;
payload diffing&amp;lt;br&amp;gt;
prevents redundant&amp;lt;br&amp;gt;
PATCH requests.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;History Heap Memory     &amp;lt; 14 MB for 50-state   Structured cloning of&amp;lt;br&amp;gt;
undo/redo buffer        lightweight UCF state&amp;lt;br&amp;gt;
trees with debounced&amp;lt;br&amp;gt;
300ms snapshot&amp;lt;br&amp;gt;
intervals.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Production War Stories &amp;amp; Solved Edge Cases&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Building a production canvas editor revealed complex graphics and&amp;lt;br&amp;gt;
browser synchronization edge cases that standard documentation&amp;lt;br&amp;gt;
overlooks.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Challenge 1: Solving Text Blurriness on High-DPI / Retina Displays&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Symptoms: Vector shapes rendered crisply, but canvas text and stroke&amp;lt;br&amp;gt;
borders appeared slightly blurry on Apple Retina screens and 4K&amp;lt;br&amp;gt;
displays.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Root Cause: Browser window.devicePixelRatio (2x or 3x) scales&amp;lt;br&amp;gt;
canvas CSS display dimensions without automatically scaling the&amp;lt;br&amp;gt;
underlying canvas backing buffer resolution.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Production Fix: Konva automatically handles pixel ratio scaling, but&amp;lt;br&amp;gt;
custom formatted text painted via HTML5 2D Canvas context (sceneFunc)&amp;lt;br&amp;gt;
required explicit scale normalization:&amp;lt;br&amp;gt;
ctx.scale(pixelRatio, pixelRatio) to ensure sub-pixel font&amp;lt;br&amp;gt;
anti-aliasing matching native DOM text.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Challenge 2: The Google Fonts Asynchronous Loading Race Condition&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Symptoms: When opening a document with custom fonts such as Playfair&amp;lt;br&amp;gt;
Display and Montserrat, text elements briefly measured with default&amp;lt;br&amp;gt;
fallback fonts, resulting in incorrect line wraps, clipped bounding&amp;lt;br&amp;gt;
boxes, and transformer handle misalignments.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Root Cause: Konva renders immediately on mount before&amp;lt;br&amp;gt;
document.fonts.load() resolves webfont TTF files.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Production Fix: We implemented a font management provider that&amp;lt;br&amp;gt;
prefetches document fonts, listens to document.fonts.ready, and&amp;lt;br&amp;gt;
triggers an atomic stage batchDraw() with text node bounding box&amp;lt;br&amp;gt;
recalculations once font glyphs are resident in GPU memory.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Challenge 3: Transformer Corner Scaling vs.&amp;nbsp;Text Box Aspect Distortion&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Symptoms: Dragging a transformer corner handle on a text box caused&amp;lt;br&amp;gt;
font characters to stretch non-uniformly (ovaled glyphs) instead of&amp;lt;br&amp;gt;
reflowing text naturally.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Root Cause: Konva Transformer applies scaleX and scaleY matrix&amp;lt;br&amp;gt;
multipliers to the target node during transform.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Production Fix: On transformend, our transform handling hook&amp;lt;br&amp;gt;
intercepts the event, resets node.scaleX(1) and node.scaleY(1), and&amp;lt;br&amp;gt;
bakes the scale multiplier directly into the text element's fontSize&amp;lt;br&amp;gt;
and width properties:&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;newFontSize = Math.round(oldFontSize * scaleX)&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;This guarantees crisp, undistorted font rendering.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Challenge 4: CSS Zoom Matrix Decoupling&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Symptoms: When users zoomed the viewport using the footer slider&amp;lt;br&amp;gt;
(50% to 200%), inline text editing text areas and crop overlays drifted&amp;lt;br&amp;gt;
away from their target shapes.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Root Cause: Canvas pan and CSS scale zoom apply outside Konva's&amp;lt;br&amp;gt;
internal coordinate matrix.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Production Fix: In our UI position calculator, overlay screen&amp;lt;br&amp;gt;
coordinates are computed by multiplying the shape's absolute Konva&amp;lt;br&amp;gt;
transform matrix by the stage's parent CSS transform scale factor:&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;clientPos = shape.getAbsolutePosition() * zoomScale + stageOffset&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Exporting UCF JSON into High-Resolution Image Views for End Users&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Once a visual document is designed and saved as Universal Canvas Format&amp;lt;br&amp;gt;
(UCF) JSON, end users need to view, share, and consume it across various&amp;lt;br&amp;gt;
client devices. Our architecture supports two distinct consumption&amp;lt;br&amp;gt;
modes.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Real-Time Interactive Canvas Rehydration&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;In web applications across desktop and mobile devices, the document&amp;lt;br&amp;gt;
viewer mounts a lightweight, read-only Konva Stage. It consumes the UCF&amp;lt;br&amp;gt;
JSON directly and renders the scene graph using the same shape&amp;lt;br&amp;gt;
dispatchers---with zero editor overhead (no toolbars, no transformer&amp;lt;br&amp;gt;
handles, no editing textarea overlays). This enables smooth interactive&amp;lt;br&amp;gt;
page flips, video playback, and clickable hyperlink hotspots.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Headless Offscreen Image Generation (PNG/WebP/PDF)&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;For generating static thumbnails, social sharing cards, downloadable&amp;lt;br&amp;gt;
PNGs, and print-ready PDFs, the application executes a client-side&amp;lt;br&amp;gt;
headless rendering pipeline:&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Offscreen Stage Mount: An invisible DOM container is dynamically&amp;lt;br&amp;gt;
created outside the visible viewport (left: -10000px) with the&amp;lt;br&amp;gt;
exact width and height of the document page.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Asset Preload Verification: The headless viewer renders the UCF&amp;lt;br&amp;gt;
scene graph and pauses capture until all remote assets (Unsplash&amp;lt;br&amp;gt;
images, Google Fonts TTF files, custom shape masks) have fully&amp;lt;br&amp;gt;
resolved.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Frame Settling: Double requestAnimationFrame() cycles allow&amp;lt;br&amp;gt;
font kerning, image decodes, and canvas clipping paths to paint&amp;lt;br&amp;gt;
completely.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;High-DPI Raster Capture: We execute&amp;lt;br&amp;gt;
stage.toDataURL({ pixelRatio: 2, mimeType: 'image/png' }) on the&amp;lt;br&amp;gt;
rendered Konva stage. Setting pixelRatio: 2 produces ultra-sharp,&amp;lt;br&amp;gt;
publication-grade raster images without blurriness or distortion.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Automatic Cleanup: Once the image data URL / Blob is resolved&amp;lt;br&amp;gt;
for download or preview, the offscreen root is safely unmounted to&amp;lt;br&amp;gt;
prevent browser memory leaks.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Engineering Lessons&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;After building this editor, five lessons stood out:&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Don't fight the browser. Use the DOM for text editing.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Keep rendering deterministic. The editor and viewer should use&amp;lt;br&amp;gt;
the same rendering engine.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Performance starts with architecture. Optimizations matter less&amp;lt;br&amp;gt;
than choosing the right rendering model.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Serialize state, not pixels. JSON scales better than images.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Invest in reusable interaction hooks. Hooks kept our codebase&amp;lt;br&amp;gt;
maintainable as the editor grew.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Tech Stack &amp;amp; Further Resources&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;The editor is built on a modern React ecosystem centered around Next.js&amp;lt;br&amp;gt;
15 (App Router) and Konva.js with React-Konva, which together provide a&amp;lt;br&amp;gt;
scalable foundation for high-performance 2D canvas rendering, scene&amp;lt;br&amp;gt;
graph management, and interactive editing. React Context manages editor&amp;lt;br&amp;gt;
state, selections, history, and document metadata, while TanStack Query&amp;lt;br&amp;gt;
and an internal API client handle data fetching, caching, and debounced&amp;lt;br&amp;gt;
autosave operations.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;The interface is styled with Tailwind&amp;lt;br&amp;gt;
CSS,&amp;lt;br&amp;gt;
typography is powered by the Google Fonts API with a custom TTF loader&amp;lt;br&amp;gt;
for accurate font rendering, and media assets are sourced through the&amp;lt;br&amp;gt;
Unsplash API and stored in cloud storage backed by a CDN. Documents are&amp;lt;br&amp;gt;
serialized into a lightweight Universal Canvas Format (UCF) JSON,&amp;lt;br&amp;gt;
enabling fast persistence, portability, and pixel-perfect rendering&amp;lt;br&amp;gt;
consistency between the editor and viewer.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Developers interested in exploring the underlying technologies can refer&amp;lt;br&amp;gt;
to the official Konva.js&amp;lt;br&amp;gt;
documentation, including the&amp;lt;br&amp;gt;
Getting Started guides, React-Konva integration guide, API Reference,&amp;lt;br&amp;gt;
Performance Tips, Select &amp;amp; Transform documentation, Interactive Sandbox&amp;lt;br&amp;gt;
examples, and the Konva and React-Konva GitHub repositories.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Core Engineering Takeaways&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Building a production-grade canvas editor requires coordination across&amp;lt;br&amp;gt;
rendering, state management, browser APIs, networking, and user&amp;lt;br&amp;gt;
experience.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Konva.js provided the rendering engine, while the surrounding&amp;lt;br&amp;gt;
architecture handled hybrid editing, history management, autosave,&amp;lt;br&amp;gt;
performance optimization, and rendering fidelity across the editor and&amp;lt;br&amp;gt;
viewer. Beyond solving interesting engineering problems, the editor&amp;lt;br&amp;gt;
transformed our document creation workflow.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Tasks that previously required external design tools and lengthy&amp;lt;br&amp;gt;
collaboration can now be completed entirely within the application in&amp;lt;br&amp;gt;
minutes, while maintaining consistent rendering between editor and&amp;lt;br&amp;gt;
viewer.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;The current architecture was intentionally designed for extensibility.&amp;lt;br&amp;gt;
Planned capabilities include collaborative real-time editing, reusable&amp;lt;br&amp;gt;
templates, version history, AI-assisted layout generation, reusable&amp;lt;br&amp;gt;
design components, and plugin-based extensibility. Because the editor is&amp;lt;br&amp;gt;
built around a scene graph and serialized document model, these features&amp;lt;br&amp;gt;
can be introduced without fundamental architectural changes.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;The architecture and lessons shared in this article can help engineering&amp;lt;br&amp;gt;
teams avoid similar pitfalls when building scalable, production-ready&amp;lt;br&amp;gt;
canvas applications.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;For teams building web applications with complex interactions and&amp;lt;br&amp;gt;
demanding performance requirements, the right frontend architecture can&amp;lt;br&amp;gt;
shape how the product scales. Our Next.js Development&amp;lt;br&amp;gt;
Services support teams&amp;lt;br&amp;gt;
in building web applications designed for performance, maintainability,&amp;lt;br&amp;gt;
and growth.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Original article:&amp;lt;br&amp;gt;
GeekyAnts&amp;lt;/p&amp;gt;
&lt;/p&gt;

</description>
      <category>design</category>
      <category>aiproductengineering</category>
      <category>ai</category>
    </item>
    <item>
      <title>The Next Wave of Mobile Apps is The Instant Prototype</title>
      <dc:creator>GeekyAnts India Pvt Ltd</dc:creator>
      <pubDate>Thu, 06 Aug 2026 10:45:04 +0000</pubDate>
      <link>https://dev.to/geekyants/the-next-wave-of-mobile-apps-is-the-instant-prototype-4aaf</link>
      <guid>https://dev.to/geekyants/the-next-wave-of-mobile-apps-is-the-instant-prototype-4aaf</guid>
      <description>&lt;p&gt;By Amrit Saluja, Technical Content Writer at GeekyAnts. Originally published on &lt;a href="https://geekyants.com/blog/the-next-wave-of-mobile-apps-is-the-instant-prototype" rel="noopener noreferrer"&gt;GeekyAnts&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Is the local IDE becoming optional? Sanket Sahu discusses the rise of vibe-coding and how browser-native tools such as RapidNative are reshaping mobile app development.&lt;/p&gt;

&lt;p&gt;Editor's note&lt;br&gt;
Sanket Sahu, co-founder of GeekyAnts and creator of gluestack, recently explored how to build an entire development server inside the browser. The engineering is interesting, but the larger question is what this architecture means for founders, product managers, and the future of enterprise AI.&lt;/p&gt;

&lt;p&gt;We spoke with Sanket about the reasons behind the build, the browser-native technologies that make it possible, and the product opportunities created by near-instant feedback. The conversation below has been edited for clarity.&lt;/p&gt;

&lt;p&gt;Is the local IDE becoming optional?&lt;br&gt;
Amrit Saluja: We hear a great deal about vibe-coding. Does browser-native architecture make the local IDE optional, or are we moving toward a hybrid future?&lt;/p&gt;

&lt;p&gt;Sanket Sahu: Vibe-coding is growing because browser-based tools remove setup friction and make &lt;a href="https://geekyants.com/service/hire-mobile-app-development-services" rel="noopener noreferrer"&gt;building apps&lt;/a&gt; more accessible. Development is moving toward a hybrid model rather than abandoning local tools altogether.&lt;/p&gt;

&lt;p&gt;What did RapidNative change?&lt;br&gt;
AS: For readers who have not seen the technical deep dive, what did you build?&lt;/p&gt;

&lt;p&gt;SS: I rebuilt the development server inside the browser. RapidNative does not require npm run dev, a CLI, or an external server. A code change appears in the preview in under 100 milliseconds while preserving application state.&lt;/p&gt;

&lt;p&gt;AS: Why replace the earlier approach?&lt;/p&gt;

&lt;p&gt;SS: The previous version ran a sandbox in the cloud. That architecture worked, but an AI tool that streams code in real time makes every network round-trip visible. The goal was not merely a fast response. It was an instant one.&lt;/p&gt;

&lt;p&gt;How can a development server run in a browser tab?&lt;br&gt;
AS: A traditional development server handles a lot of work. How did you move those responsibilities into the browser?&lt;/p&gt;

&lt;p&gt;SS: A server such as Metro watches the file system, transpiles code, bundles modules, and serves the result over HTTP. We replaced each responsibility with a browser-native capability. Service Workers take the place of the HTTP server. IndexedDB and a virtual file system replace the physical file system. Babel Standalone performs transpilation, while Import Maps remove the need to bundle modules during development.&lt;/p&gt;

&lt;p&gt;AS: That sounds similar to Vite. Was it an influence?&lt;/p&gt;

&lt;p&gt;SS: Yes. Both approaches begin with the same observation: modern browsers understand ES modules, so development does not always need a bundling step. Vite still relies on &lt;a href="https://geekyants.com/hire-nodejs-developers" rel="noopener noreferrer"&gt;Node.js&lt;/a&gt; and a CLI. RapidNative extends the idea by moving the full development loop into the browser.&lt;/p&gt;

&lt;p&gt;Why use two virtual file systems?&lt;br&gt;
AS: What makes the dual-VFS architecture necessary?&lt;/p&gt;

&lt;p&gt;SS: The two file systems have separate responsibilities. The Source VFS holds the original TypeScript and JSX files. When a source file changes, the browser transforms it and writes plain JavaScript into the Destination VFS. The Service Worker serves only from that destination. Keeping source and output separate makes the pipeline predictable and fast.&lt;/p&gt;

&lt;p&gt;What does a sub-100ms preview mean for founders?&lt;br&gt;
AS: How does that preview speed help a founder or a non-technical decision-maker?&lt;/p&gt;

&lt;p&gt;SS: Fast iteration protects the creative flow. It helps teams produce better MVPs, test their thinking sooner, and reach a first paying user faster. For many people, seeing an app idea running on a phone in less than two minutes is the moment the product becomes real.&lt;/p&gt;

&lt;p&gt;AS: How is RapidNative different from Expo Snack?&lt;/p&gt;

&lt;p&gt;SS: The products serve different purposes. Snack is a browser-based REPL that helps developers test snippets and run them on devices. RapidNative focuses on building full apps with AI assistance and instant browser feedback. Its sub-100ms update loop matters when an AI is continuously streaming code changes.&lt;/p&gt;

&lt;p&gt;Who is RapidNative for today?&lt;br&gt;
AS: Is the current product aimed at startups or enterprises?&lt;/p&gt;

&lt;p&gt;SS: Today, it is best suited to individuals and startups that want to turn an idea into a working app. Customization, team features, and compliance capabilities are on the roadmap so that agencies and enterprises can adopt it as well.&lt;/p&gt;

&lt;p&gt;What comes next?&lt;br&gt;
AS: What remains on the technical roadmap?&lt;/p&gt;

&lt;p&gt;SS: Three major additions are planned: a TypeScript Language Server for editor autocomplete, browser-native Git operations through isomorphic-git, and native-device support through Expo Go. Together, they should combine instant browser feedback with the advantages of running an app on a real device.&lt;/p&gt;

&lt;p&gt;AS: You have built NativeBase, gluestack, and now RapidNative. Has AI changed your goal?&lt;/p&gt;

&lt;p&gt;SS: The goal remains the same: remove friction for developers, designers, founders, and product managers. AI is an enabler and an accelerator. It shortens the distance between imagining an idea and seeing it work on a screen.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mobile</category>
      <category>app</category>
    </item>
  </channel>
</rss>
