<?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: Axon OSINT</title>
    <description>The latest articles on DEV Community by Axon OSINT (@axonosint).</description>
    <link>https://dev.to/axonosint</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4203110%2Fb0cd7b98-aba3-4ca8-88e2-32d0ae87a89d.png</url>
      <title>DEV Community: Axon OSINT</title>
      <link>https://dev.to/axonosint</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/axonosint"/>
    <language>en</language>
    <item>
      <title>Our React App Worked Perfectly—Until Someone Closed Their Laptop</title>
      <dc:creator>Axon OSINT</dc:creator>
      <pubDate>Mon, 26 Oct 2026 21:07:45 +0000</pubDate>
      <link>https://dev.to/axon-osint/our-react-app-worked-perfectly-until-someone-closed-their-laptop-3628</link>
      <guid>https://dev.to/axon-osint/our-react-app-worked-perfectly-until-someone-closed-their-laptop-3628</guid>
      <description>&lt;p&gt;&lt;em&gt;What sleep/wake bugs taught us about Supabase, network recovery, and building reliable SaaS applications.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Imagine spending weeks building a React application.&lt;/p&gt;

&lt;p&gt;Authentication works. Database queries work. Real-time updates work. Your dashboard loads quickly, and your end-to-end tests are passing.&lt;/p&gt;

&lt;p&gt;You deploy it.&lt;/p&gt;

&lt;p&gt;Everything looks great.&lt;/p&gt;

&lt;p&gt;Then someone closes their laptop.&lt;/p&gt;

&lt;p&gt;They return an hour later, open the lid, and continue working.&lt;/p&gt;

&lt;p&gt;The application is still there. They're still logged in. The buttons are visible.&lt;/p&gt;

&lt;p&gt;But something is wrong.&lt;/p&gt;

&lt;p&gt;A request never finishes. A loading indicator keeps spinning. A piece of information disappears from the screen.&lt;/p&gt;

&lt;p&gt;Refreshing the page fixes everything.&lt;/p&gt;

&lt;p&gt;Sound familiar?&lt;/p&gt;

&lt;p&gt;This became one of the most interesting reliability challenges we encountered while building &lt;strong&gt;Axon OSINT&lt;/strong&gt;, a SaaS platform for digital investigations.&lt;/p&gt;

&lt;p&gt;And the lesson was bigger than any particular library:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A web application that works after a fresh page load is not necessarily an application that survives real life.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The Bug That Didn't Look Like a Bug
&lt;/h2&gt;

&lt;p&gt;One of the frustrating things about sleep/wake failures is that the application often doesn't crash.&lt;/p&gt;

&lt;p&gt;There's no dramatic error screen.&lt;/p&gt;

&lt;p&gt;No obvious exception.&lt;/p&gt;

&lt;p&gt;No broken deployment.&lt;/p&gt;

&lt;p&gt;Instead, the failure can be subtle.&lt;/p&gt;

&lt;p&gt;A dashboard displays information from an earlier session. A request stays pending. An action appears to be running, but nothing happens.&lt;/p&gt;

&lt;p&gt;During our work on Axon OSINT, we encountered variations of these problems across multiple areas, including investigation workspaces, account settings, and billing workflows.&lt;/p&gt;

&lt;p&gt;The recurring pattern was particularly interesting.&lt;/p&gt;

&lt;p&gt;A fresh reload could restore normal behavior.&lt;/p&gt;

&lt;p&gt;But the application was supposed to keep working without asking users to refresh their browsers every time they returned to their desks.&lt;/p&gt;

&lt;p&gt;That raised a question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What exactly changes when a laptop sleeps, if the React application itself is still mounted?&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Your JavaScript State Can Survive Longer Than Your Network State
&lt;/h2&gt;

&lt;p&gt;When a browser tab goes into the background or a computer sleeps, several things can change.&lt;/p&gt;

&lt;p&gt;JavaScript timers may be delayed.&lt;/p&gt;

&lt;p&gt;Network connections may be interrupted.&lt;/p&gt;

&lt;p&gt;Authentication credentials may approach expiration.&lt;/p&gt;

&lt;p&gt;Real-time connections may disconnect.&lt;/p&gt;

&lt;p&gt;An asynchronous operation that was started earlier may not complete when expected.&lt;/p&gt;

&lt;p&gt;Meanwhile, React component state can remain in memory.&lt;/p&gt;

&lt;p&gt;So after the user returns, the interface may still look exactly as it did before.&lt;/p&gt;

&lt;p&gt;This creates a dangerous mismatch:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The UI remembers a healthy application, while the network may no longer be healthy.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Consider a basic loading pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;loadData&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;setLoading&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="kd"&gt;const&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;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;items&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&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="nf"&gt;setItems&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="nf"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code has an immediate problem: if the operation throws, &lt;code&gt;setLoading(false)&lt;/code&gt; is never reached.&lt;/p&gt;

&lt;p&gt;We can improve that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;loadData&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;setLoading&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;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&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="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;items&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &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="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nf"&gt;setItems&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="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="nf"&gt;showError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="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;Better.&lt;/p&gt;

&lt;p&gt;But there's still a problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if the promise never settles within a reasonable amount of time?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;finally&lt;/code&gt; block won't execute until the awaited operation settles.&lt;/p&gt;

&lt;p&gt;A loading indicator can remain visible indefinitely if nothing imposes a deadline.&lt;/p&gt;

&lt;p&gt;This became one of our most important rules:&lt;/p&gt;

&lt;p&gt;Every critical network action needs a predictable way to finish.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. We Stopped Treating the SDK as the Only Transport
&lt;/h2&gt;

&lt;p&gt;Axon OSINT uses React, TypeScript, Supabase, and Zustand.&lt;/p&gt;

&lt;p&gt;Supabase provides a convenient SDK for database operations, authentication, and other backend functionality.&lt;/p&gt;

&lt;p&gt;Under normal conditions, the SDK is exactly what we want.&lt;/p&gt;

&lt;p&gt;But our post-wake engineering standard addresses a specific failure mode: after sleep or tab suspension, some SDK-dependent operations may stall even while the application still appears usable.&lt;/p&gt;

&lt;p&gt;Our response was not to abandon Supabase.&lt;/p&gt;

&lt;p&gt;Instead, we separated two concerns:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;What operation does the application need to perform?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Which transport can safely perform it right now?&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For critical operations, our architecture can use the SDK with a bounded timeout and an alternative direct HTTP route when appropriate.&lt;/p&gt;

&lt;p&gt;Supabase exposes REST and RPC endpoints, so not every operation has to depend on the same client-side execution path.&lt;/p&gt;

&lt;p&gt;A simplified read strategy looks 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;User requests data
        |
        v
Is the SDK marked degraded?
        |
   +----+----+
   |         |
  Yes        No
   |         |
   v         v
Direct    Try SDK with
HTTP      a timeout
             |
        +----+----+
        |         |
      Success    Failure
        |         |
        v         v
      Return    Direct HTTP
      data      fallback
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The distinction matters.&lt;/p&gt;

&lt;p&gt;If we already know that the SDK has become unreliable in the current browser session, repeating the same problematic path can waste time.&lt;/p&gt;

&lt;p&gt;Instead, eligible reads can use the alternative route immediately.&lt;/p&gt;

&lt;p&gt;This is a resilience pattern, not a universal recommendation to bypass every SDK whenever something goes wrong.&lt;/p&gt;

&lt;p&gt;The fallback still needs correct authentication, authorization, error handling, and data validation.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. A Timeout Is a Deadline, Not a Recovery Strategy
&lt;/h2&gt;

&lt;p&gt;A timeout is useful because it prevents the UI from waiting forever.&lt;/p&gt;

&lt;p&gt;But a timeout alone doesn't solve the underlying problem.&lt;/p&gt;

&lt;p&gt;Here's a simplified helper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;withTimeout&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PromiseLike&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;timeoutMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&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;race&lt;/span&gt;&lt;span class="p"&gt;([&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;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;never&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;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&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;setTimeout&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;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Operation timed out&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;timeoutMs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This illustration puts a deadline on waiting.&lt;/p&gt;

&lt;p&gt;However, it doesn't actually cancel the underlying network request.&lt;/p&gt;

&lt;p&gt;That's an important difference.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Promise.race()&lt;/code&gt; can stop your code from awaiting an operation, while the original request may still be running.&lt;/p&gt;

&lt;p&gt;For cancellable direct HTTP requests, &lt;code&gt;AbortController&lt;/code&gt; is a better tool for enforcing network deadlines.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fetchWithDeadline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;init&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RequestInit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;timeoutMs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8000&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;controller&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;AbortController&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;timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&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;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;timeoutMs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&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;init&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`HTTP &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important design decision isn't whether to use 8, 10, or 15 seconds.&lt;/p&gt;

&lt;p&gt;It's that the operation has an explicit boundary.&lt;/p&gt;

&lt;p&gt;After that boundary, the application must make a decision.&lt;/p&gt;

&lt;p&gt;Retry safely?&lt;/p&gt;

&lt;p&gt;Use a different transport?&lt;/p&gt;

&lt;p&gt;Display an error?&lt;/p&gt;

&lt;p&gt;Preserve previously loaded data?&lt;/p&gt;

&lt;p&gt;It cannot simply remain stuck.&lt;/p&gt;

&lt;p&gt;One crucial caveat: &lt;strong&gt;a timed-out write may have succeeded on the server&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Blindly retrying a payment, record creation, or other non-idempotent action can introduce duplicate operations.&lt;/p&gt;

&lt;p&gt;Writes need stronger safeguards, such as idempotency keys, reconciliation, or a status check before retrying.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. We Introduced a Session-Level Degraded Mode
&lt;/h2&gt;

&lt;p&gt;One useful decision in our architecture was to remember when a critical SDK operation had timed out.&lt;/p&gt;

&lt;p&gt;Rather than repeatedly trying the same failing path, we introduced a session-level degraded state.&lt;/p&gt;

&lt;p&gt;The concept is straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;SdkHealth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;degraded&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a critical operation encounters a qualifying failure, the application can mark the SDK as degraded for the current browser session.&lt;/p&gt;

&lt;p&gt;Then subsequent eligible actions can avoid unnecessary waits.&lt;/p&gt;

&lt;p&gt;This isn't about declaring the entire backend offline.&lt;/p&gt;

&lt;p&gt;The backend might be completely healthy.&lt;/p&gt;

&lt;p&gt;The problem may exist in a particular browser-side request path.&lt;/p&gt;

&lt;p&gt;It's an important distinction:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An unhealthy client connection does not necessarily mean an unhealthy server.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Our architecture deliberately keeps this degraded state until a hard page reload, rather than repeatedly switching back and forth using an automatic timer.&lt;/p&gt;

&lt;p&gt;Why?&lt;/p&gt;

&lt;p&gt;Because oscillating between healthy and degraded modes can create another category of problems.&lt;/p&gt;

&lt;p&gt;One request works. Another hangs. The system retries. Then the user clicks again.&lt;/p&gt;

&lt;p&gt;Suddenly one intermittent failure has become a cascade of activity.&lt;/p&gt;

&lt;p&gt;Stability sometimes means choosing a conservative path and sticking to it for the session.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Authentication Recovery Must Not Depend on the Broken Path
&lt;/h2&gt;

&lt;p&gt;There is another subtle failure mode.&lt;/p&gt;

&lt;p&gt;Imagine you've detected an SDK problem and decided to use direct HTTP.&lt;/p&gt;

&lt;p&gt;Before the fallback request, you try to retrieve the access token using another asynchronous SDK call.&lt;/p&gt;

&lt;p&gt;What happens if that call depends on the same degraded client behavior?&lt;/p&gt;

&lt;p&gt;Your fallback can hang too.&lt;/p&gt;

&lt;p&gt;This is why our internal post-wake standard calls for using the application's already-maintained authentication state when appropriate.&lt;/p&gt;

&lt;p&gt;In Axon OSINT, Zustand stores session information used by critical recovery flows.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SDK request becomes unreliable
           |
           v
Use a verified, existing session token
           |
           v
Make the authorized fallback request
           |
           v
Handle success, expiration, or failure
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This does &lt;strong&gt;not&lt;/strong&gt; mean ignoring authentication expiration or inventing a token.&lt;/p&gt;

&lt;p&gt;A cached token must still be valid for the request, and the server must enforce authorization on every path.&lt;/p&gt;

&lt;p&gt;If no valid token is available, the application should follow a controlled authentication recovery flow.&lt;/p&gt;

&lt;p&gt;A fallback is not a security bypass.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resilience and authorization must work together.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Recovering Every Component at Once Can Make Things Worse
&lt;/h2&gt;

&lt;p&gt;At first, it might seem that the solution to wake-related bugs is simple:&lt;/p&gt;

&lt;p&gt;Whenever the browser tab becomes visible, refetch everything.&lt;/p&gt;

&lt;p&gt;Unfortunately, this can introduce an entirely new problem.&lt;/p&gt;

&lt;p&gt;Imagine your application has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An investigations list.&lt;/li&gt;
&lt;li&gt;A graph workspace.&lt;/li&gt;
&lt;li&gt;A notification panel.&lt;/li&gt;
&lt;li&gt;Account settings.&lt;/li&gt;
&lt;li&gt;A billing summary.&lt;/li&gt;
&lt;li&gt;Several background queries.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If every component independently reacts to a &lt;code&gt;focus&lt;/code&gt; or &lt;code&gt;visibilitychange&lt;/code&gt; event, returning to the tab can trigger many simultaneous requests.&lt;/p&gt;

&lt;p&gt;Some may overlap.&lt;/p&gt;

&lt;p&gt;Others may compete for state updates.&lt;/p&gt;

&lt;p&gt;A few might fail while the others recover.&lt;/p&gt;

&lt;p&gt;We've had to consider this problem in Axon OSINT as well.&lt;/p&gt;

&lt;p&gt;Our recovery architecture tracks wake cycles and avoids treating every focus event as a completely new recovery operation.&lt;/p&gt;

&lt;p&gt;It also distinguishes between ordinary tab focus and a situation that actually requires recovery.&lt;/p&gt;

&lt;p&gt;The key lesson:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recover the application, not every component independently.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One coordinated recovery process is easier to reason about than a dozen unrelated event listeners.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. The UI Must Be Honest About What It Knows
&lt;/h2&gt;

&lt;p&gt;Here's a particularly dangerous response to network failure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;loadSubscription&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="kc"&gt;null&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;At first glance, it looks harmless.&lt;/p&gt;

&lt;p&gt;But what does &lt;code&gt;null&lt;/code&gt; mean?&lt;/p&gt;

&lt;p&gt;Does the user have no subscription?&lt;/p&gt;

&lt;p&gt;Or did the network fail?&lt;/p&gt;

&lt;p&gt;Those are completely different states.&lt;/p&gt;

&lt;p&gt;If the application treats a transport failure as an absence of business data, it may hide information that still exists on the server.&lt;/p&gt;

&lt;p&gt;The same issue applies to returning an empty array when a request fails.&lt;/p&gt;

&lt;p&gt;An empty list might mean there are no investigations.&lt;/p&gt;

&lt;p&gt;Or it might mean the application couldn't load them.&lt;/p&gt;

&lt;p&gt;A reliable interface should distinguish these cases.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;LoadResult&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stale&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;message&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the application can display previously confirmed information without pretending that a failed network request returned an empty result.&lt;/p&gt;

&lt;p&gt;But cached information must be clearly understood as potentially outdated.&lt;/p&gt;

&lt;p&gt;And sensitive actions should still require fresh, authorized server confirmation.&lt;/p&gt;

&lt;p&gt;This distinction became especially important for workflows involving account status, permissions, and billing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A network error is not a business decision.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  9. We Learned That Background Writes Need Their Own Rules
&lt;/h2&gt;

&lt;p&gt;Our investigation workspace includes graph-based interactions.&lt;/p&gt;

&lt;p&gt;Users may move entities around, organize relationships, and update layouts.&lt;/p&gt;

&lt;p&gt;Some of these operations generate background writes.&lt;/p&gt;

&lt;p&gt;Imagine moving dozens of nodes, then returning from sleep while the application attempts to persist every position at once.&lt;/p&gt;

&lt;p&gt;A naive implementation might look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="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;nodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;savePosition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&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 a large graph, this can create a burst of network requests.&lt;/p&gt;

&lt;p&gt;If the network is degraded, the application may produce multiple timeouts and errors, even though saving node positions is not the user's most important task.&lt;/p&gt;

&lt;p&gt;Our engineering guidelines therefore distinguish between critical actions and non-critical persistence.&lt;/p&gt;

&lt;p&gt;For position updates and similar background operations, we favor concepts such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Queuing.&lt;/li&gt;
&lt;li&gt;Deduplication.&lt;/li&gt;
&lt;li&gt;Debouncing.&lt;/li&gt;
&lt;li&gt;Bounded concurrency.&lt;/li&gt;
&lt;li&gt;Summarized failures.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The user should be able to continue interacting with the graph without a non-critical persistence problem freezing the entire interface.&lt;/p&gt;

&lt;p&gt;But the application must also avoid pretending that unsaved changes were successfully persisted.&lt;/p&gt;

&lt;p&gt;The right trade-off depends on the operation.&lt;/p&gt;

&lt;p&gt;A graph position and a financial transaction should never share the same retry policy.&lt;/p&gt;




&lt;h2&gt;
  
  
  10. The Tests That Matter Happen After the Happy Path
&lt;/h2&gt;

&lt;p&gt;A typical frontend test might do this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Open the page
Click a button
Verify the result
Done
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;But it doesn't cover the conditions that exposed our reliability problems.&lt;/p&gt;

&lt;p&gt;Our sleep/wake test scenarios also consider situations such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Open the application
Authenticate
Open an investigation
Put the laptop to sleep
Wake it
Try to create an entity
Switch between graph and table
Verify the UI reaches a terminal state
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Other scenarios cover network interruption, security settings, account sessions, and post-wake actions.&lt;/p&gt;

&lt;p&gt;The desired result isn't always that every request succeeds.&lt;/p&gt;

&lt;p&gt;Sometimes the network genuinely isn't available.&lt;/p&gt;

&lt;p&gt;What matters is that the application behaves predictably.&lt;/p&gt;

&lt;p&gt;No infinite loading.&lt;/p&gt;

&lt;p&gt;No blank screen.&lt;/p&gt;

&lt;p&gt;No uncontrolled request storm.&lt;/p&gt;

&lt;p&gt;No misleading empty data.&lt;/p&gt;

&lt;p&gt;No duplicate writes caused by unsafe retries.&lt;/p&gt;

&lt;p&gt;And no action that leaves the user wondering whether it happened.&lt;/p&gt;

&lt;p&gt;That final point matters more than it might sound.&lt;/p&gt;

&lt;p&gt;For users, uncertainty about whether an action succeeded can be worse than a clear error message.&lt;/p&gt;




&lt;h2&gt;
  
  
  11. Our Reliability Checklist Changed
&lt;/h2&gt;

&lt;p&gt;After working through these issues, we began asking different questions during development.&lt;/p&gt;

&lt;p&gt;Not just:&lt;/p&gt;

&lt;p&gt;"Does this action work?"&lt;/p&gt;

&lt;p&gt;But:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does it work after the browser has been suspended?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens if the SDK request stalls?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can the user recover without refreshing the entire page?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Will a timeout leave an action stuck in loading state?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Could retrying this operation create duplicate data?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does an empty response mean no records exist, or that something failed?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Will multiple components initiate recovery simultaneously?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does the fallback preserve the same authorization boundaries?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;These questions are now part of how we approach critical workflows.&lt;/p&gt;

&lt;p&gt;And there is an important qualification: architecture standards don't automatically eliminate every regression.&lt;/p&gt;

&lt;p&gt;New features can still introduce bugs if they don't follow the same rules.&lt;/p&gt;

&lt;p&gt;Reliability is a property that has to be maintained, not a checkbox that can be completed once.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Most Important Lesson
&lt;/h2&gt;

&lt;p&gt;When we started building Axon OSINT, our attention was naturally focused on features.&lt;/p&gt;

&lt;p&gt;Investigations. Entities. Relationships. Collaboration. Billing. AI-assisted workflows.&lt;/p&gt;

&lt;p&gt;All of those are important.&lt;/p&gt;

&lt;p&gt;But real users don't always interact with applications under ideal conditions.&lt;/p&gt;

&lt;p&gt;They close their laptops.&lt;/p&gt;

&lt;p&gt;They switch between tabs.&lt;/p&gt;

&lt;p&gt;Their internet connection drops.&lt;/p&gt;

&lt;p&gt;Their session changes while the application is in the background.&lt;/p&gt;

&lt;p&gt;They return to something they started earlier and expect to continue.&lt;/p&gt;

&lt;p&gt;That is normal behavior.&lt;/p&gt;

&lt;p&gt;And a well-designed SaaS application must treat it as normal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The real test of a web application isn't only whether it works when everything goes right. It's whether it remains understandable, secure, and recoverable when something goes wrong.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's one of the biggest engineering lessons we've learned while building Axon OSINT.&lt;/p&gt;

&lt;p&gt;And we're still learning.&lt;/p&gt;




&lt;h3&gt;
  
  
  Let's Discuss
&lt;/h3&gt;

&lt;p&gt;Have you experienced bugs that only appear after laptop sleep, browser suspension, or network reconnection?&lt;/p&gt;

&lt;p&gt;How do you handle stalled requests, background recovery, and duplicate-action prevention in your React applications?&lt;/p&gt;

&lt;p&gt;I'd love to hear what has worked for you.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;We're building &lt;a href="https://axonosint.com" rel="noopener noreferrer"&gt;Axon OSINT&lt;/a&gt;, a platform for digital investigations, OSINT research, and collaborative evidence analysis.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article is part of our **Building Axon OSINT&lt;/em&gt;* engineering series. The code snippets are simplified educational illustrations, not verbatim production code.*&lt;/p&gt;

</description>
      <category>react</category>
      <category>webdev</category>
      <category>supabase</category>
      <category>typescript</category>
    </item>
    <item>
      <title>We Built an AI Screenshot-to-Report System. The Hard Part Was Knowing When Not to Trust the AI.</title>
      <dc:creator>Axon OSINT</dc:creator>
      <pubDate>Fri, 23 Oct 2026 17:37:29 +0000</pubDate>
      <link>https://dev.to/axon-osint/we-built-an-ai-screenshot-to-report-system-the-hard-part-was-knowing-when-not-to-trust-the-ai-35p5</link>
      <guid>https://dev.to/axon-osint/we-built-an-ai-screenshot-to-report-system-the-hard-part-was-knowing-when-not-to-trust-the-ai-35p5</guid>
      <description>&lt;p&gt;&lt;em&gt;How we're building a fraud-reporting experience with React, TypeScript, Supabase, and Gemini — and why human review matters more than a perfect-looking AI demo.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Imagine receiving this message:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Your package couldn't be delivered. Pay €2.95 to arrange another delivery."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There's a link underneath it.&lt;/p&gt;

&lt;p&gt;You don't recognize the sender. The website looks legitimate, but something feels wrong.&lt;/p&gt;

&lt;p&gt;Now imagine opening a fraud-reporting website and seeing a form asking you to identify the incident category, communication channel, suspicious domain, financial impact, relevant evidence, and contact details.&lt;/p&gt;

&lt;p&gt;For a developer, these fields probably make sense.&lt;/p&gt;

&lt;p&gt;For someone who is worried about losing money, they might not.&lt;/p&gt;

&lt;p&gt;That difference became an important part of what we're building at &lt;strong&gt;Axon OSINT&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;We wanted people to report suspicious activity without first having to understand the vocabulary of fraud investigations.&lt;/p&gt;

&lt;p&gt;And we started with something almost everyone knows how to create:&lt;/p&gt;

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

&lt;p&gt;But we discovered that converting a screenshot into structured information is only one part of the problem.&lt;/p&gt;

&lt;p&gt;The more important question is what happens when the AI makes a mistake.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The Problem Isn't Just Extracting Text
&lt;/h2&gt;

&lt;p&gt;Let's start with a simple example.&lt;/p&gt;

&lt;p&gt;A screenshot of a suspicious message may contain several different kinds of information:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The original message.&lt;/li&gt;
&lt;li&gt;A phone number or username.&lt;/li&gt;
&lt;li&gt;A website address.&lt;/li&gt;
&lt;li&gt;A payment amount.&lt;/li&gt;
&lt;li&gt;The name of a company someone is impersonating.&lt;/li&gt;
&lt;li&gt;A date or timestamp.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those details are not interchangeable.&lt;/p&gt;

&lt;p&gt;Consider the difference between these two statements:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Observed fact:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"The screenshot contains a link to &lt;code&gt;parcel-check.example&lt;/code&gt;."&lt;/p&gt;

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

&lt;p&gt;"The link belongs to a fraudulent delivery service."&lt;/p&gt;

&lt;p&gt;The first statement describes something visible in the image.&lt;/p&gt;

&lt;p&gt;The second makes a judgment about what it means.&lt;/p&gt;

&lt;p&gt;That distinction is critical.&lt;/p&gt;

&lt;p&gt;An AI system might correctly extract a URL while being wrong about the intentions of the sender. It might also misunderstand part of a message, confuse two numbers, or suggest the wrong incident category.&lt;/p&gt;

&lt;p&gt;In an ordinary productivity application, a slightly incorrect summary can be inconvenient.&lt;/p&gt;

&lt;p&gt;In a fraud report, an incorrect phone number or website address can make the information significantly less useful.&lt;/p&gt;

&lt;p&gt;So we designed the screenshot feature around a simple principle:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI should help organize what the user provides, not decide what happened on the user's behalf.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Our Architecture: A Draft Before a Report
&lt;/h2&gt;

&lt;p&gt;Our frontend uses React and TypeScript. Supabase handles parts of the backend, and Gemini is used for optional screenshot analysis.&lt;/p&gt;

&lt;p&gt;The simplified flow looks 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;User chooses screenshot upload
              |
              v
     File and age checks
              |
              v
   Screenshot analysis request
              |
              v
          Gemini
    Structured extraction
              |
              v
     Normalize the result
              |
              v
    Editable incident draft
              |
              v
     User reviews details
              |
              v
   Consent and submission
              |
              v
    Report confirmation
              |
              v
 Restricted report processing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One of the most important decisions in this flow is that the AI result does not automatically become a submitted incident.&lt;/p&gt;

&lt;p&gt;It becomes a draft.&lt;/p&gt;

&lt;p&gt;The user can review the extracted information, correct errors, add missing details, and decide whether to submit the report.&lt;/p&gt;

&lt;p&gt;This keeps two very different operations separate:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Assistance:&lt;/strong&gt; helping someone prepare information.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Submission:&lt;/strong&gt; recording information that the person has reviewed and chosen to provide.&lt;/p&gt;

&lt;p&gt;That separation also makes the system easier to reason about from a security and privacy perspective.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. A Screenshot Isn't Trusted Input
&lt;/h2&gt;

&lt;p&gt;The first boundary is surprisingly ordinary: file validation.&lt;/p&gt;

&lt;p&gt;Our screenshot analysis endpoint accepts JPEG, PNG, and WebP images, with a 5 MB limit.&lt;/p&gt;

&lt;p&gt;We validate the file type and size before sending it for analysis.&lt;/p&gt;

&lt;p&gt;Why does this matter?&lt;/p&gt;

&lt;p&gt;Because an upload feature is not just an image picker. It is a boundary between data supplied by an unknown user and a service that consumes external resources.&lt;/p&gt;

&lt;p&gt;We also have to remember that everything &lt;em&gt;inside&lt;/em&gt; the screenshot is untrusted.&lt;/p&gt;

&lt;p&gt;A suspicious message might contain a URL, a payment instruction, or text claiming to come from a trusted organization.&lt;/p&gt;

&lt;p&gt;We don't want the analysis process to treat those instructions as commands.&lt;/p&gt;

&lt;p&gt;For example, if the screenshot says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Visit this website immediately to verify your identity."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The AI's job is to extract that message, not follow its instructions.&lt;/p&gt;

&lt;p&gt;Our extraction prompt explicitly tells the model to work only with visible information, not invent missing values, and not open or check links.&lt;/p&gt;

&lt;p&gt;This is an important architectural boundary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Analyzing a suspicious link is not the same as visiting it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For a reporting workflow, those should be separate capabilities with separate controls.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Structured Output Is Useful. It Isn't Proof of Accuracy.
&lt;/h2&gt;

&lt;p&gt;Instead of asking Gemini to return an essay, we request structured information.&lt;/p&gt;

&lt;p&gt;The response can include:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;message_text&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Original message from the screenshot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;platform_used&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Where the message appeared&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;suggested_scenario&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Possible incident category&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;entities&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;URLs, phone numbers, accounts, and other identifiers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;description&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Suggested explanation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;confidence&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Confidence category&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;warnings&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Information the user should verify&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This structure makes it possible to map the extracted result into fields in the React form.&lt;/p&gt;

&lt;p&gt;It also makes handling missing or uncertain information more predictable.&lt;/p&gt;

&lt;p&gt;But a JSON response is not automatically a correct response.&lt;/p&gt;

&lt;p&gt;A model can return perfectly valid JSON containing incorrect information.&lt;/p&gt;

&lt;p&gt;That's why we distinguish between structural validity and factual accuracy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A valid schema tells us the result is in the expected format. It doesn't tell us that the model understood the screenshot correctly.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens when confidence is low?
&lt;/h3&gt;

&lt;p&gt;Our analysis layer supports three confidence categories:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;high&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;medium&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;low&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When the result has low confidence, the system falls back to a generic incident category rather than presenting a specific classification as reliable.&lt;/p&gt;

&lt;p&gt;It also asks the user to verify the extracted details.&lt;/p&gt;

&lt;p&gt;Here's a simplified TypeScript example illustrating the principle. It is not a copy of our production implementation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Confidence&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;high&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;medium&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;low&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Extraction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Confidence&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;message_text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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="nl"&gt;suggested_scenario&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="nl"&gt;entities&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;type&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="nl"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createEditableDraft&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Extraction&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="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="na"&gt;scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;confidence&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;low&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;other_fraud_message&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;suggested_scenario&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="na"&gt;identifiers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;entities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})),&lt;/span&gt;

    &lt;span class="na"&gt;requiresUserReview&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the last field.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;requiresUserReview: true&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;It doesn't become &lt;code&gt;false&lt;/code&gt; when confidence is high.&lt;/p&gt;

&lt;p&gt;That's intentional.&lt;/p&gt;

&lt;p&gt;A confidence label is useful information, but it should not remove the user's ability to inspect and correct the result.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Never Rewrite the Evidence to Make It Look Cleaner
&lt;/h2&gt;

&lt;p&gt;One of the most interesting challenges in this kind of application is the difference between translating an explanation and changing the original evidence.&lt;/p&gt;

&lt;p&gt;Suppose a suspicious message contains:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Send 250 EUR to this account.
Contact @payment_support_247.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The user might be completing the report in German, French, Ukrainian, or English.&lt;/p&gt;

&lt;p&gt;We want the interface and generated explanations to use the selected language.&lt;/p&gt;

&lt;p&gt;But the original message and identifiers must retain their original meaning and characters.&lt;/p&gt;

&lt;p&gt;Imagine an automatic translation changing a username, modifying a website address, or replacing a financial identifier.&lt;/p&gt;

&lt;p&gt;Even a tiny modification could undermine a later comparison.&lt;/p&gt;

&lt;p&gt;Our extraction instructions therefore distinguish between:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Human-readable explanation:&lt;/strong&gt; may be generated in the user's selected language.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Original message and identifiers:&lt;/strong&gt; should be preserved as they appear.&lt;/p&gt;

&lt;p&gt;The same principle applies to URLs, domains, phone numbers, and usernames.&lt;/p&gt;

&lt;p&gt;Sometimes, what appears to be a typo is the most important detail in the entire report.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;trusted-service.example
trusted-servlce.example
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These strings look similar, but they are not identical.&lt;/p&gt;

&lt;p&gt;Automatically "correcting" one into the other would destroy information that might matter.&lt;/p&gt;

&lt;p&gt;This led us to a broader design principle:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Normalize data for comparison without silently rewriting the original observation.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That distinction is useful far beyond fraud reporting — especially in logging, compliance, investigations, and document-processing systems.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. We Added a Path That Doesn't Require AI
&lt;/h2&gt;

&lt;p&gt;It's tempting to make AI the center of every modern application.&lt;/p&gt;

&lt;p&gt;We deliberately avoided making it mandatory.&lt;/p&gt;

&lt;p&gt;Our reporting interface provides three entry points:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Manual entry.&lt;/strong&gt; The user describes the incident and enters the information themselves.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Screenshot assistance.&lt;/strong&gt; The user uploads an image, and the system prepares an editable draft.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Voice assistance.&lt;/strong&gt; The user records or uploads audio, and the system helps turn their explanation into a draft.&lt;/p&gt;

&lt;p&gt;These are different ways to reach the same goal.&lt;/p&gt;

&lt;p&gt;Not everyone is comfortable uploading screenshots. Some users prefer typing. Others find it easier to explain what happened verbally.&lt;/p&gt;

&lt;p&gt;And some people simply don't want their material processed by an external AI provider.&lt;/p&gt;

&lt;p&gt;For that reason, the interface explains that optional screenshot and voice tools send the selected material to Google Gemini for draft extraction before the final report is submitted.&lt;/p&gt;

&lt;p&gt;Users who want to avoid that AI processing can choose manual entry.&lt;/p&gt;

&lt;p&gt;There is also an important distinction between screenshots and voice recordings.&lt;/p&gt;

&lt;p&gt;A submitted screenshot can be retained as report evidence in restricted storage. The voice recording, by contrast, is used to prepare the draft and is not stored as an evidence recording in this workflow.&lt;/p&gt;

&lt;p&gt;These differences matter.&lt;/p&gt;

&lt;p&gt;A privacy notice shouldn't be a decorative element that appears after the important decisions have already been made.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;People need to understand what happens to their information at the point where they choose to provide it.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  7. The Unexpected UX Problem: The Screenshot Is on the Wrong Device
&lt;/h2&gt;

&lt;p&gt;Another practical problem has very little to do with machine learning.&lt;/p&gt;

&lt;p&gt;Imagine someone reading a fraud-reporting form on a laptop.&lt;/p&gt;

&lt;p&gt;The suspicious message is on their phone.&lt;/p&gt;

&lt;p&gt;How should they upload it?&lt;/p&gt;

&lt;p&gt;A developer might download the screenshot from cloud storage, email it to themselves, or use a nearby-device sharing feature.&lt;/p&gt;

&lt;p&gt;But requiring those extra steps creates friction.&lt;/p&gt;

&lt;p&gt;So we built a QR-based upload flow.&lt;/p&gt;

&lt;p&gt;The computer displays a QR code. The user scans it with their phone and uploads the screenshot from there.&lt;/p&gt;

&lt;p&gt;The desktop then receives the analysis result and continues the report.&lt;/p&gt;

&lt;p&gt;The user doesn't need to manually transfer the image to their computer first.&lt;/p&gt;

&lt;p&gt;There are several interesting engineering considerations behind this relatively simple interaction.&lt;/p&gt;

&lt;h3&gt;
  
  
  Temporary sessions
&lt;/h3&gt;

&lt;p&gt;The upload flow uses a time-limited session rather than a permanently reusable upload address.&lt;/p&gt;

&lt;p&gt;The interface shows the remaining time and handles expiration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Asynchronous state
&lt;/h3&gt;

&lt;p&gt;The desktop doesn't assume the upload happens immediately.&lt;/p&gt;

&lt;p&gt;The client tracks states such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;creating
qr_ready
waiting_for_upload
analyzing
analyzed
expired
failed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These states let us communicate what is happening without leaving the user guessing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Failure recovery
&lt;/h3&gt;

&lt;p&gt;The user can retry or choose another reporting method if the session expires or the analysis fails.&lt;/p&gt;

&lt;p&gt;This is a small feature, but it taught us an important lesson:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reducing friction in a real user's workflow can be just as valuable as improving the AI model itself.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The best model doesn't help if the user can't conveniently provide the input.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. Privacy Isn't a Checkbox at the End
&lt;/h2&gt;

&lt;p&gt;Fraud reports may contain sensitive personal information.&lt;/p&gt;

&lt;p&gt;They can include messages, payment details, contact information, and descriptions of potentially harmful events.&lt;/p&gt;

&lt;p&gt;That creates responsibilities beyond ordinary form validation.&lt;/p&gt;

&lt;p&gt;In Axon OSINT, submitted incident information is not intended to become a public social feed or a searchable collection of personal reports.&lt;/p&gt;

&lt;p&gt;The reporting workflow uses restricted storage and backend access controls. Access to sensitive information depends on authorization and applicable access rules, not simply on whether someone has created an account.&lt;/p&gt;

&lt;p&gt;This separation becomes especially important when information from different reports is compared.&lt;/p&gt;

&lt;p&gt;Imagine three reports mentioning the same suspicious domain.&lt;/p&gt;

&lt;p&gt;That repetition may be useful.&lt;/p&gt;

&lt;p&gt;But it doesn't mean everyone searching for the domain should be able to read those three reports.&lt;/p&gt;

&lt;p&gt;We separate the idea of &lt;strong&gt;a matching warning signal&lt;/strong&gt; from &lt;strong&gt;access to the underlying statements and evidence&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Our public checking feature can return limited information about matches in previously reported data without opening those personal statements to the person performing the search.&lt;/p&gt;

&lt;p&gt;And even when several reports contain the same number or website, that does not automatically prove a crime or identify the person responsible.&lt;/p&gt;

&lt;p&gt;A repeated identifier is a signal worth examining, not a verdict.&lt;/p&gt;

&lt;p&gt;For applications handling sensitive user submissions, this separation between source records and limited public results is an important design pattern.&lt;/p&gt;




&lt;h2&gt;
  
  
  9. The Submission Boundary Is Deliberately Separate
&lt;/h2&gt;

&lt;p&gt;Another detail we care about is confirmation.&lt;/p&gt;

&lt;p&gt;The system supports people submitting reports without first creating a full user account.&lt;/p&gt;

&lt;p&gt;An email confirmation flow helps establish that a report was submitted using the stated address.&lt;/p&gt;

&lt;p&gt;The backend also validates required acknowledgements, consent information, and report details during submission.&lt;/p&gt;

&lt;p&gt;This matters because the application should not treat an AI-generated draft, a user-submitted report, and a confirmed report as identical.&lt;/p&gt;

&lt;p&gt;They represent different levels of user action and confirmation.&lt;/p&gt;

&lt;p&gt;The distinction is also relevant to downstream analysis.&lt;/p&gt;

&lt;p&gt;For example, the documented cross-incident pattern-processing path requires the appropriate consent and confirmed report status.&lt;/p&gt;

&lt;p&gt;This doesn't eliminate the need for careful access control or data validation.&lt;/p&gt;

&lt;p&gt;But it reduces the risk of treating every unverified input as equally authoritative.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good data processing starts with knowing where the data came from and what has actually been confirmed.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  10. What We'd Tell Developers Building Something Similar
&lt;/h2&gt;

&lt;p&gt;Several principles from this work apply to other applications using AI to turn unstructured content into structured records.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keep extraction separate from action
&lt;/h3&gt;

&lt;p&gt;A model can suggest information without being allowed to submit, publish, or act on it.&lt;/p&gt;

&lt;p&gt;This is particularly important when errors could affect another person.&lt;/p&gt;

&lt;h3&gt;
  
  
  Preserve the original input
&lt;/h3&gt;

&lt;p&gt;A clean summary is convenient. The original words and identifiers may be essential.&lt;/p&gt;

&lt;p&gt;Don't make it impossible to recover what the user actually provided.&lt;/p&gt;

&lt;h3&gt;
  
  
  Treat confidence as a warning, not a guarantee
&lt;/h3&gt;

&lt;p&gt;Confidence should influence how the result is presented, not replace verification.&lt;/p&gt;

&lt;p&gt;A high-confidence mistake is still a mistake.&lt;/p&gt;

&lt;h3&gt;
  
  
  Design for failure before celebrating success
&lt;/h3&gt;

&lt;p&gt;What happens if the upload fails? The AI returns incomplete information? The QR session expires?&lt;/p&gt;

&lt;p&gt;A graceful manual fallback is part of the feature, not an afterthought.&lt;/p&gt;

&lt;h3&gt;
  
  
  Explain third-party processing before it happens
&lt;/h3&gt;

&lt;p&gt;If user-provided content will be sent to an AI provider, make that clear before the user chooses the feature.&lt;/p&gt;

&lt;p&gt;Privacy should be understandable, not merely technically documented.&lt;/p&gt;

&lt;h3&gt;
  
  
  Separate useful signals from sensitive records
&lt;/h3&gt;

&lt;p&gt;Some information can help identify repeated patterns without exposing the people who provided it.&lt;/p&gt;

&lt;p&gt;Design those boundaries explicitly.&lt;/p&gt;




&lt;h2&gt;
  
  
  11. The Questions We're Still Working On
&lt;/h2&gt;

&lt;p&gt;Building this kind of system doesn't mean all the difficult questions are solved.&lt;/p&gt;

&lt;p&gt;We continue to think about issues such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How can we better communicate uncertainty without overwhelming nontechnical users?&lt;/li&gt;
&lt;li&gt;How should an interface highlight fields that deserve additional review?&lt;/li&gt;
&lt;li&gt;How can we make screenshot-assisted reporting easier for people with limited digital experience?&lt;/li&gt;
&lt;li&gt;How should systems prevent repeated but misleading reports from creating excessive confidence in a suspicious indicator?&lt;/li&gt;
&lt;li&gt;How can cross-incident analysis remain useful while respecting privacy and authorization boundaries?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are product questions, security questions, and engineering questions at the same time.&lt;/p&gt;

&lt;p&gt;And they deserve more attention than simply choosing a new model.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Most Important Part of Our AI Feature Is Still the Human
&lt;/h2&gt;

&lt;p&gt;We started with a seemingly straightforward idea:&lt;/p&gt;

&lt;p&gt;Take a screenshot. Extract the relevant information. Help the user complete a report.&lt;/p&gt;

&lt;p&gt;But the deeper we got into the workflow, the clearer the priorities became.&lt;/p&gt;

&lt;p&gt;It wasn't enough to make AI capable of reading a suspicious message.&lt;/p&gt;

&lt;p&gt;We also needed to make the result understandable, editable, and appropriately restricted.&lt;/p&gt;

&lt;p&gt;The system needed a way to express uncertainty.&lt;/p&gt;

&lt;p&gt;It needed a fallback when automatic processing failed.&lt;/p&gt;

&lt;p&gt;And it needed a clear boundary between an AI suggestion and an action taken by the user.&lt;/p&gt;

&lt;p&gt;We are building this as part of &lt;strong&gt;Axon OSINT&lt;/strong&gt;, a platform focused on digital investigations, open-source intelligence, and responsible fraud reporting.&lt;/p&gt;

&lt;p&gt;For developers, the most useful takeaway is one that applies well beyond our project:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't measure an AI-assisted workflow only by how impressive its output looks. Measure it by how safely and clearly it behaves when the output is wrong.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's where an interesting demo begins to become a dependable product.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; If you're building an application that turns screenshots, documents, or audio into structured data, how do you handle uncertainty and human review? Which mistakes are unacceptable in your domain?&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Project context: &lt;a href="https://axonosint.com" rel="noopener noreferrer"&gt;Axon OSINT&lt;/a&gt; · &lt;a href="https://axonosint.com/report-fraud" rel="noopener noreferrer"&gt;Public fraud-reporting workflow&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>react</category>
      <category>security</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
