<?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: Gathmo</title>
    <description>The latest articles on DEV Community by Gathmo (@gathmo).</description>
    <link>https://dev.to/gathmo</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%2F3992687%2F6f2aff7a-544f-4952-99bf-e2369405a9cf.png</url>
      <title>DEV Community: Gathmo</title>
      <link>https://dev.to/gathmo</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gathmo"/>
    <language>en</language>
    <item>
      <title>Building a company in Nigeria through a 3-day power outage</title>
      <dc:creator>Gathmo</dc:creator>
      <pubDate>Fri, 09 Oct 2026 13:01:16 +0000</pubDate>
      <link>https://dev.to/gathmo/building-a-company-in-nigeria-through-a-3-day-power-outage-1mea</link>
      <guid>https://dev.to/gathmo/building-a-company-in-nigeria-through-a-3-day-power-outage-1mea</guid>
      <description>&lt;p&gt;The power went off twice today while I was writing this article, a few minutes each time. In Ibadan that's a normal day.&lt;br&gt;
Last week it lasted three days. A fault on NEPA's side, as people here still call the power company, cut off our house while the rest of the street had power.&lt;br&gt;
I'm German and left Germany more than six years ago. From Ibadan I build Gathmo (&lt;a href="https://gathmo.com" rel="noopener noreferrer"&gt;https://gathmo.com&lt;/a&gt;), an app where guests at weddings and parties scan a QR code and add their photos and videos to one shared album. At the moment I'm extending it with six sister sites for other countries. I'm also building TrafficBot (&lt;a href="https://traffic-bot.com" rel="noopener noreferrer"&gt;https://traffic-bot.com&lt;/a&gt;), CTR software for SEO agencies. AI agents write the code for both, and those agents run on my laptop. Once the battery is empty, my development stops.&lt;br&gt;
On the first day the laptop battery carried me. After that my power bank ran empty and the phone network was my only way online. I packed the laptop and the power bank and moved to Horizon Suites, a hotel here in Ibadan, to charge everything and get internet again. That cost extra.&lt;br&gt;
In those three days I built almost nothing new. I answered customers and sorted out a few small things they had pointed out. At home I work from an office with a proper desk, a good chair and air conditioning. The hotel had none of that, and with the tension of the whole situation I couldn't focus on new features.&lt;br&gt;
The servers kept running, since they sit outside Nigeria. Anyone using Gathmo or TrafficBot had no reason to notice. Only the development on my laptop stopped.&lt;br&gt;
When the battery runs low, I change how I work. I run fewer agents at the same time, so the laptop draws less power. I also make sure everything is written down, including the checklist each agent works through, so I can pick the work up again without trouble once the power is back.&lt;br&gt;
Moving the agents to a cloud server wouldn't solve this for me. If my laptop and my phone are both empty, I can't reach the cloud either.&lt;br&gt;
A normal week looks calmer. I pay for a premium electricity tariff and get around 20 hours of power a day. About once a week an outage lasts long enough that I switch to a 50,000 mAh power bank, and long agent runs that need serious computing power drain it fast. With power I have an MTN line with 200 Mbit. Without it, I fall back to the phone network.&lt;br&gt;
I don't plan my day around the power. I charge the power bank whenever there is power, because if I skip that once, I can't work through the next outage.&lt;br&gt;
I don't type the code myself. I started building with ChatGPT 3.5 in 2022, copying snippets between the chat window and my editor. Four years later, agents build whole websites for me. I plan the site and set its direction, then pick one of the three layouts the agent proposes. At the end I test whether the product is easy and fun to use. Depending on the task I work with Claude Code, Codex or DeepSeek.&lt;br&gt;
I came to Nigeria from Vietnam for a fintech plan. A fellow developer, who built and runs the payments app AidaPay (aidapay.ng), and I wanted to buy a banking license. The Central Bank of Nigeria wasn't issuing new ones when we looked and prices for existing licenses had exploded, so we went into a partnership with Moniepoint instead. Both Gathmo and TrafficBot started here in Nigeria.&lt;br&gt;
Payments from here work fine. As a German I could set up Stripe without trouble, and I added Paystack to Gathmo so customers in Nigeria and other African countries can pay. Stripe doesn't cover most of those payments, and Paystack has worked well for me.&lt;br&gt;
Next I'm choosing between a second power bank and an inverter, so a day or two without power doesn't stop my agents anymore. If you build from somewhere with unreliable power, which one made the difference for you?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>buildinpublic</category>
      <category>startup</category>
    </item>
    <item>
      <title>Keep automated sessions separate in a CSV report: a small Python example</title>
      <dc:creator>Gathmo</dc:creator>
      <pubDate>Sun, 04 Oct 2026 07:16:58 +0000</pubDate>
      <link>https://dev.to/trafficbotcom/keep-automated-sessions-separate-in-a-csv-report-a-small-python-example-mkj</link>
      <guid>https://dev.to/trafficbotcom/keep-automated-sessions-separate-in-a-csv-report-a-small-python-example-mkj</guid>
      <description>&lt;p&gt;A report can have correct arithmetic and still be misleading. If automated activity and human visits enter the same total, a reader may interpret every session as a potential customer.&lt;/p&gt;

&lt;p&gt;This example keeps three supplied labels separate: &lt;code&gt;automated&lt;/code&gt;, &lt;code&gt;human&lt;/code&gt; and &lt;code&gt;unknown&lt;/code&gt;. It validates a small CSV and returns one total for each label. It does not detect bots or decide whether a visitor is human. Those labels must come from evidence you maintain outside this script.&lt;/p&gt;

&lt;p&gt;Disclosure: this guide is published by the operator of &lt;a href="https://traffic-bot.com/" rel="noopener noreferrer"&gt;Traffic Bot&lt;/a&gt;, which provides CTR software for SEO agencies. An AI agent drafted this guide and code; the example and validation cases were run locally before publication.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Define the input contract
&lt;/h2&gt;

&lt;p&gt;Save the following fictional data as &lt;code&gt;sessions.csv&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;activity_class,sessions
automated,120
human,80
unknown,20
automated,120
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each row represents an aggregate from the same reporting window. The &lt;code&gt;sessions&lt;/code&gt; value is a non-negative integer, and the label describes the evidence available for that aggregate. Do not relabel uncertain activity as human merely because you cannot identify it as automated. Use &lt;code&gt;unknown&lt;/code&gt; when the classification is unresolved.&lt;/p&gt;

&lt;p&gt;Use mutually exclusive aggregates. If the same sessions appear in two rows from different exports, this script will add both rows; it cannot identify the overlap. Deduplicate or reconcile the inputs before summarizing them.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Validate before adding
&lt;/h2&gt;

&lt;p&gt;Save this as &lt;code&gt;session_totals.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;

&lt;span class="n"&gt;CLASSES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;automated&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;human&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;summarize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;totals&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;CLASSES&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;newline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DictReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fieldnames&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;activity_class&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sessions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Expected activity_class,sessions headers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;activity_class&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sessions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;totals&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Invalid class or extra field on line &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isascii&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isdecimal&lt;/span&gt;&lt;span class="p"&gt;()):&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Expected non-negative integer on line &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;totals&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;totals&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;summarize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Python’s &lt;a href="https://docs.python.org/3/library/csv.html#csv.DictReader" rel="noopener noreferrer"&gt;&lt;code&gt;csv.DictReader&lt;/code&gt;&lt;/a&gt; maps a CSV row to header names. The code opens the file with &lt;code&gt;newline=""&lt;/code&gt;, checks the exact two-column header and rejects extra fields, unexpected labels, missing counts and non-integer counts.&lt;/p&gt;

&lt;p&gt;This is a small reporting example, not a full ingestion pipeline. It stops on the first invalid row. In a larger workflow, capture the error alongside the input file and fix the source data rather than silently dropping the row.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Run the example
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 session_totals.py sessions.csv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output from the local run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;automated: 240
human: 80
unknown: 20
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two automated rows add to 240. The other totals remain separate. These numbers are fictional sample data, not product results or customer figures.&lt;/p&gt;

&lt;p&gt;The validation checks also rejected a negative count, a decimal count, an unsupported label, an empty count, an extra field and duplicate headers. Try one of those inputs and the script should raise &lt;code&gt;ValueError&lt;/code&gt; rather than produce a plausible total from invalid data.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Keep the report’s claims as narrow as the data
&lt;/h2&gt;

&lt;p&gt;A total labelled &lt;code&gt;human&lt;/code&gt; still does not prove purchase intent or a sale. Record enquiries and completed purchases from their own evidence. Likewise, an automated-session total does not establish an improvement in search visibility.&lt;/p&gt;

&lt;p&gt;Include the reporting dates, source files and classification method when you hand the totals to another person. If the inputs combine different time windows or inconsistent classification rules, correcting the arithmetic will not fix the comparison.&lt;/p&gt;

&lt;p&gt;Traffic Bot runs automated Google search-and-visit sessions for indexed client pages already appearing within the Top 100 for a chosen keyword. Campaigns use the Traffic Creator account and SEO Wallet. These sessions are automated, not human customers, and rankings or sales are not guaranteed. The product’s current offer and operator details are available at &lt;a href="https://traffic-bot.com/" rel="noopener noreferrer"&gt;traffic-bot.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>python</category>
      <category>tutorial</category>
      <category>analytics</category>
    </item>
    <item>
      <title>The upload reached 100%. The photo was still missing.</title>
      <dc:creator>Gathmo</dc:creator>
      <pubDate>Fri, 25 Sep 2026 17:28:33 +0000</pubDate>
      <link>https://dev.to/gathmo/the-upload-reached-100-the-photo-was-still-missing-5ff1</link>
      <guid>https://dev.to/gathmo/the-upload-reached-100-the-photo-was-still-missing-5ff1</guid>
      <description>&lt;p&gt;An upload bar can tell the truth and still mislead the person watching it.&lt;/p&gt;

&lt;p&gt;The browser may have sent every byte to object storage. That does not mean the photo has entered the album. We learned to treat those as separate events while building &lt;a href="https://gathmo.com/" rel="noopener noreferrer"&gt;Gathmo&lt;/a&gt;, an event album where guests contribute media from their phones.&lt;/p&gt;

&lt;p&gt;This matters at a wedding or conference. A guest does not care which request failed. They see a completed progress bar, close the tab, and expect the picture to be there.&lt;/p&gt;

&lt;h2&gt;
  
  
  There are two finish lines
&lt;/h2&gt;

&lt;p&gt;Our upload flow has a short version:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The browser prepares the file and asks the API to start an upload.&lt;/li&gt;
&lt;li&gt;The API returns a temporary URL for object storage.&lt;/li&gt;
&lt;li&gt;The browser sends the bytes directly to storage.&lt;/li&gt;
&lt;li&gt;The browser tells the API the upload finished.&lt;/li&gt;
&lt;li&gt;The API validates and processes the media before it appears in the album.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Step 3 can succeed while step 4 fails. The file exists, but the database record is still pending. A progress bar that reaches 100% at step 3 has measured &lt;strong&gt;bytes transferred&lt;/strong&gt;, not &lt;strong&gt;album readiness&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That distinction changed the interface we needed. After the byte transfer, the guest sees a finalizing state. Success comes only after the API acknowledges the completion request. It also changed the debugging we needed: a failure after storage received the file deserves its own error category. Retrying the entire upload at that point wastes time and bandwidth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Large files exposed a different problem
&lt;/h2&gt;

&lt;p&gt;Small photos and large videos do not behave the same way on a busy venue network. If a single request carrying a video breaks near the end, the next attempt starts from the beginning. Sending the same bytes again is especially painful when a phone is about to lock or the guest is leaving the page.&lt;/p&gt;

&lt;p&gt;For larger files, our API offers a multipart upload. The browser sends parts to storage and keeps track of which ones completed. If one part fails because the connection drops, the retry unit is &lt;strong&gt;that part&lt;/strong&gt;, not the whole video.&lt;/p&gt;

&lt;p&gt;That sounds obvious until the retry wrapper sits around the wrong function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Wrong boundary: part 3 fails, so parts 1 and 2 go again.&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;retry&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;uploadEveryPart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;// Better boundary: only the failed part goes again.&lt;/span&gt;
&lt;span class="k"&gt;for &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;part&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;retry&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;uploadPart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;part&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 real implementation also needs to retain each completed part's ETag. Storage needs that list, in order, when the upload is assembled. If the browser resumes an open upload, it asks which parts are already present and skips them.&lt;/p&gt;

&lt;p&gt;There are limits to this recovery. If the page is gone and its local file is no longer available, the browser cannot invent the missing bytes. Resume helps when the guest retries with the file still available in the active session. It is not a promise that every abandoned upload completes itself later.&lt;/p&gt;

&lt;h2&gt;
  
  
  More parallel uploads were not always faster
&lt;/h2&gt;

&lt;p&gt;Our queue permits a few uploads at once, but only one large file at a time. That choice came from looking at where large uploads stalled: several videos started together, competed for the same connection, and none got far enough to leave a useful completed part.&lt;/p&gt;

&lt;p&gt;Serializing the large files lets one make progress while small photos can still pass through the queue. It is a less impressive number for a concurrency setting. For a guest on unreliable Wi-Fi, it is a better chance of getting something into the album.&lt;/p&gt;

&lt;p&gt;We also pause a transient retry while the browser reports that it is offline. Running through three retries in the few seconds before the phone reconnects is not resilience; it is just using up the retry budget.&lt;/p&gt;

&lt;h2&gt;
  
  
  The server still has to check the file
&lt;/h2&gt;

&lt;p&gt;The browser declares a file type, a byte count, and a hash before upload. Those values help with routing and duplicate detection, but the browser is not a trusted validator. After storage receives the object, the server checks the real object size and its file header before continuing with processing.&lt;/p&gt;

&lt;p&gt;That is another reason I would avoid calling an upload “done” when the PUT request ends. There are several states between &lt;em&gt;bytes arrived&lt;/em&gt; and &lt;em&gt;this media is ready for the album&lt;/em&gt;. Collapsing them into one green checkmark makes support harder and leaves guests with the wrong expectation.&lt;/p&gt;

&lt;p&gt;The lesson for us was practical: measure progress at the layer that owns it. Transfer progress belongs to the browser. Completion belongs to the API. Album availability belongs to the processing pipeline. When something breaks, record which boundary it crossed. That tells you whether to retry a part, retry a completion call, or investigate processing—without asking a guest to send the whole video again.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>typescript</category>
      <category>debugging</category>
      <category>programming</category>
    </item>
    <item>
      <title>Checksums in Browser Uploads: What They Prove and What They Do Not</title>
      <dc:creator>Gathmo</dc:creator>
      <pubDate>Fri, 17 Jul 2026 18:47:55 +0000</pubDate>
      <link>https://dev.to/gathmo/checksums-in-browser-uploads-what-they-prove-and-what-they-do-not-4af</link>
      <guid>https://dev.to/gathmo/checksums-in-browser-uploads-what-they-prove-and-what-they-do-not-4af</guid>
      <description>&lt;p&gt;Checksums are often added to upload protocols with an imprecise promise of “security.” A digest can detect accidental corruption and identify whether two byte sequences match. It does not prove who created a file, whether consent exists, or whether the media is safe to display.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choose the integrity boundary
&lt;/h2&gt;

&lt;p&gt;Decide which bytes are hashed. The original file selected in the browser, each uploaded part, the assembled object, and a transcoded derivative are different byte sequences. Record separate digests when the system needs to verify each boundary.&lt;/p&gt;

&lt;p&gt;For an original-preserving service, the most useful comparison is browser source digest versus assembled storage-object digest. Large files should be streamed rather than copied into one enormous ArrayBuffer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understand browser limits
&lt;/h2&gt;

&lt;p&gt;The standard Web Crypto digest API expects the full input buffer, which can cause memory pressure for videos. Incremental hashing requires a carefully reviewed library or a worker implementation. Do not freeze the main thread while presenting a “preparing” spinner.&lt;/p&gt;

&lt;p&gt;For small photographs:&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;bytes&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;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;digest&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;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SHA-256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For large media, hash parts during transfer and compute or verify the final object digest on the server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part hashes are not automatically a file hash
&lt;/h2&gt;

&lt;p&gt;Concatenating hexadecimal part hashes does not produce the SHA-256 of the complete file. A multipart storage ETag may also use a provider-specific algorithm. Document exactly what each field represents.&lt;/p&gt;

&lt;p&gt;If the protocol verifies every numbered part and the server assembles them in a trusted order, part digests provide transfer integrity. A separate final digest can protect export and handover workflows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bind metadata to the digest
&lt;/h2&gt;

&lt;p&gt;A digest by itself has no event, media ID, or algorithm context. Store it with the upload intent and immutable byte length. Include an algorithm version so future migrations do not reinterpret old values.&lt;/p&gt;

&lt;p&gt;Do not use a raw digest as a public object key for private media. Someone who already has a common file can test for its presence. Keep identifiers random and authorization separate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handle mismatch safely
&lt;/h2&gt;

&lt;p&gt;A mismatch should prevent acceptance and trigger a bounded retry or restart. Preserve a safe reason code and enough part-level evidence to diagnose network or implementation problems. Do not log file contents or signed URLs.&lt;/p&gt;

&lt;p&gt;Repeated mismatches from one client version may indicate a hashing bug rather than hostile traffic. Compare across coarse browser and application versions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use digests for deduplication carefully
&lt;/h2&gt;

&lt;p&gt;Server-side deduplication can save storage, but cross-event equality leaks information and complicates deletion. Prefer deduplication within one event or one owner boundary. Keep reference counts correct and ensure deleting one media record does not remove bytes still required elsewhere.&lt;/p&gt;

&lt;p&gt;Perceptual hashes answer a different question and create additional privacy risks. They should not be introduced as a casual extension of byte-integrity checks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify exports too
&lt;/h2&gt;

&lt;p&gt;The digest recorded at acceptance can appear in an export manifest. The archive builder should hash the exact exported bytes and note when a derivative differs from the original. Recipients can then confirm download integrity.&lt;/p&gt;

&lt;p&gt;I help build Gathmo and have a commercial interest in trustworthy media handling. The &lt;a href="https://gathmo.com/about" rel="noopener noreferrer"&gt;Gathmo company overview&lt;/a&gt; gives product context; these checksum boundaries apply to many upload and archive systems.&lt;/p&gt;

&lt;p&gt;A checksum is valuable when its byte boundary, algorithm, storage, and failure behavior are explicit. It is evidence of equality—not a substitute for authorization, moderation, provenance, or consent.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>security</category>
    </item>
    <item>
      <title>Fairness and Backpressure in Real-Time Moderation Queues</title>
      <dc:creator>Gathmo</dc:creator>
      <pubDate>Fri, 17 Jul 2026 18:47:39 +0000</pubDate>
      <link>https://dev.to/gathmo/fairness-and-backpressure-in-real-time-moderation-queues-4eg0</link>
      <guid>https://dev.to/gathmo/fairness-and-backpressure-in-real-time-moderation-queues-4eg0</guid>
      <description>&lt;p&gt;A live event moderation queue can receive hundreds of uploads in a few minutes. Processing strictly in arrival order sounds fair, but a single guest sending large videos can delay photographs from everyone else. Unlimited parallelism creates the opposite problem: storage, scanning, and preview workers collapse together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate pipeline stages
&lt;/h2&gt;

&lt;p&gt;Use distinct queues for validation, malware scanning, metadata extraction, preview generation, and human review. Each stage has different resource costs and failure policies.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;accepted -&amp;gt; validate -&amp;gt; scan -&amp;gt; derive -&amp;gt; reviewable -&amp;gt; approved
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A file should carry a stable media ID and stage version so retries do not produce duplicate derivatives or repeated review entries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Apply admission control
&lt;/h2&gt;

&lt;p&gt;The control plane should limit active work per event and per contributor session. Admission control does not need to reject every excess upload; it can place the item in a durable waiting state and tell the browser that acceptance succeeded.&lt;/p&gt;

&lt;p&gt;Distinguish “accepted but queued” from “processing.” This prevents the UI from showing a stalled progress bar while the system intentionally waits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Schedule by cost class
&lt;/h2&gt;

&lt;p&gt;Classify photographs, short audio, and video into coarse cost bands. Use weighted scheduling so small items continue to flow while expensive video jobs receive guaranteed capacity.&lt;/p&gt;

&lt;p&gt;A simple pattern is separate queues with worker weights, for example six image slots, two audio slots, and two video slots. Revisit weights using observed processing time, not file count alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prevent one contributor from dominating
&lt;/h2&gt;

&lt;p&gt;Round-robin across contributor sessions or use a per-session concurrency cap. The organizer may also assign priority to official staff uploads, but that policy should be explicit and visible in audit records.&lt;/p&gt;

&lt;p&gt;Do not use hidden device fingerprints to create fairness identities. An event-local random session is enough for rate and concurrency controls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Propagate backpressure
&lt;/h2&gt;

&lt;p&gt;When downstream scanning is saturated, upstream derivative workers should slow rather than writing millions of pending messages. Expose queue depth and oldest-item age. Scale on both values: depth alone can look healthy while one poisoned job waits for hours.&lt;/p&gt;

&lt;p&gt;Set maximum retry counts and move deterministic failures to a dead-letter path with the media ID, stage, safe error code, and attempt history. Never include signed URLs or private captions in queue logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design the moderator view
&lt;/h2&gt;

&lt;p&gt;The human queue should not reorder itself under the moderator's cursor. Give each item a stable position or a clear “new items” boundary. Show media type, event-local context, processing state, and reason for any automated flag.&lt;/p&gt;

&lt;p&gt;Actions must be idempotent. Two moderators approving the same item should converge on one state and produce an audit record, not two notifications.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recover from worker loss
&lt;/h2&gt;

&lt;p&gt;Use visibility timeouts or leases, renew them during long video work, and make each stage safe to retry. A worker crash after writing a preview but before acknowledging the job should detect and reuse the existing derivative.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test burst behavior
&lt;/h2&gt;

&lt;p&gt;Replay event-shaped traffic: many photos after a speech, a few large videos, and simultaneous moderator activity. Measure time to first reviewable item, p95 queue age by media type, duplicate work, and fairness across sessions.&lt;/p&gt;

&lt;p&gt;I help build Gathmo and have a commercial interest in moderated event media. The &lt;a href="https://gathmo.com/corporate" rel="noopener noreferrer"&gt;Gathmo corporate workflow&lt;/a&gt; is one use case; these queue patterns apply to any bursty user-generated-content pipeline.&lt;/p&gt;

&lt;p&gt;Backpressure is not a failure. It is the mechanism that preserves predictable service and fair progress when demand exceeds one stage's capacity.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>webdev</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Offline Recovery for Browser Uploads Without False Promises</title>
      <dc:creator>Gathmo</dc:creator>
      <pubDate>Fri, 17 Jul 2026 18:47:24 +0000</pubDate>
      <link>https://dev.to/gathmo/offline-recovery-for-browser-uploads-without-false-promises-5371</link>
      <guid>https://dev.to/gathmo/offline-recovery-for-browser-uploads-without-false-promises-5371</guid>
      <description>&lt;p&gt;“Works offline” is an attractive promise for an event upload page. In practice, a browser cannot send media without a network, may not retain a selected File after restart, and cannot rely on background execution across mobile platforms. A useful offline design is honest about those constraints.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define what offline means
&lt;/h2&gt;

&lt;p&gt;There are several different capabilities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the page shell can open from cache;&lt;/li&gt;
&lt;li&gt;selected-file metadata survives a temporary disconnect;&lt;/li&gt;
&lt;li&gt;an in-progress chunk can retry when connectivity returns;&lt;/li&gt;
&lt;li&gt;the full original file survives a page reload;&lt;/li&gt;
&lt;li&gt;upload continues after the browser closes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not group them under one badge. Test and document each supported behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cache only the application shell
&lt;/h2&gt;

&lt;p&gt;A service worker can cache HTML, CSS, JavaScript, and static instructions. Avoid caching private album responses or signed upload URLs. Version the shell and provide a safe fallback when an old client talks to a new API.&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="nb"&gt;self&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;fetch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isStaticAsset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;respondWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;cacheFirst&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Event state should come from the network when available and fail closed when authorization cannot be checked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Persist resumable metadata
&lt;/h2&gt;

&lt;p&gt;IndexedDB can store upload IDs, committed parts, file size, type, and expiry. It usually cannot guarantee access to the original File after a restart. The interface should say “Reconnect to continue” for a live page and “Select the same file to resume” after a full restart.&lt;/p&gt;

&lt;p&gt;Verify reselected files with name, size, modification time, and an optional partial fingerprint. Do not upload a different file into the old session merely because the filename matches.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat connectivity events as hints
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;online&lt;/code&gt; and &lt;code&gt;offline&lt;/code&gt; events do not prove that the upload host is reachable. Captive portals, DNS problems, and blocked storage domains are common. Pause after repeated network failures and probe the control API with a bounded timeout.&lt;/p&gt;

&lt;p&gt;When connectivity returns, fetch committed parts before resending. A response may have been lost while the server stored the bytes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Be cautious with Background Sync
&lt;/h2&gt;

&lt;p&gt;One-off Background Sync is not uniformly supported and may not run promptly. Passing large File objects from a closed page to a worker is especially unreliable. Use it as an optimization for small control messages, not as the only path for important media.&lt;/p&gt;

&lt;p&gt;Keep the page awake only when the user has chosen to continue, and avoid hidden battery-draining loops.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design the recovery UI
&lt;/h2&gt;

&lt;p&gt;Show each file separately with states such as waiting for connection, ready to resume, needs re-selection, expired, or accepted. Preserve the guest's progress and explain the next action. A global red banner is less useful than per-file recovery.&lt;/p&gt;

&lt;p&gt;If the event closes while offline, do not discard the local record immediately. Tell the guest to keep the original and offer the organizer's support route if appropriate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test real interruptions
&lt;/h2&gt;

&lt;p&gt;Use airplane mode, a captive portal, screen lock, tab eviction, browser restart, and Wi-Fi-to-cellular transitions. Test on actual iOS and Android devices. Development tools that toggle “offline” do not reproduce mobile lifecycle behavior.&lt;/p&gt;

&lt;p&gt;I help build Gathmo and have a commercial interest in resilient event uploads. The &lt;a href="https://gathmo.com/how-it-works" rel="noopener noreferrer"&gt;Gathmo no-app browser flow&lt;/a&gt; provides product context; the recovery principles here apply to other PWAs and upload tools.&lt;/p&gt;

&lt;p&gt;A trustworthy offline experience never implies that bytes have left the phone when they have not. It preserves safe state, detects reconnection, reconciles with the server, and tells the user exactly what remains to do.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>pwa</category>
    </item>
    <item>
      <title>Polling, SSE, or WebSockets for Mobile Upload Status?</title>
      <dc:creator>Gathmo</dc:creator>
      <pubDate>Fri, 17 Jul 2026 18:47:05 +0000</pubDate>
      <link>https://dev.to/gathmo/polling-sse-or-websockets-for-mobile-upload-status-4m55</link>
      <guid>https://dev.to/gathmo/polling-sse-or-websockets-for-mobile-upload-status-4m55</guid>
      <description>&lt;p&gt;After the browser transfers a file, the server may still scan, transcode, extract metadata, or generate previews. The interface needs status updates, but the most fashionable real-time transport is not automatically the most reliable choice for an event guest on a mobile browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the update contract
&lt;/h2&gt;

&lt;p&gt;Define states and transitions before choosing transport:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;accepted -&amp;gt; queued -&amp;gt; processing -&amp;gt; ready
accepted -&amp;gt; rejected
processing -&amp;gt; processing_error
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every status response should include a monotonically increasing version or timestamp. The client can ignore events older than the state it already knows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Polling is a strong baseline
&lt;/h2&gt;

&lt;p&gt;HTTP polling works through proxies, resumes naturally after a page wake, and is easy to cache and rate-limit. Use adaptive intervals: one second just after acceptance, then back off to several seconds when processing takes longer.&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;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="nx"&gt;slowChecks&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add jitter when many guests finish at the same time. Stop when the state is terminal, the page is gone, or a server-provided retry time says to wait longer.&lt;/p&gt;

&lt;h2&gt;
  
  
  SSE fits one-way progress
&lt;/h2&gt;

&lt;p&gt;Server-Sent Events are attractive when the server only pushes status. The browser reconnects with a last-event ID, and the wire format is simple. But mobile networks and some intermediaries silently kill idle connections.&lt;/p&gt;

&lt;p&gt;Send occasional heartbeats and treat reconnection as normal. The reconnect handler must fetch current state because events may have been missed beyond the server replay window.&lt;/p&gt;

&lt;p&gt;Do not keep one SSE stream per file. Subscribe by guest session or album and filter authorized upload IDs on the server.&lt;/p&gt;

&lt;h2&gt;
  
  
  WebSockets add more responsibility
&lt;/h2&gt;

&lt;p&gt;WebSockets make sense when the client also sends frequent commands over the same channel or when a host moderation console needs many rapid updates. For a guest waiting on two files, they add connection authentication, heartbeat, reconnect, replay, and load-balancer complexity without much user benefit.&lt;/p&gt;

&lt;p&gt;Never rely on the socket as the only source of truth. A reconnect must reconcile through an HTTP status endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design transport fallback
&lt;/h2&gt;

&lt;p&gt;The client can prefer SSE and fall back to polling after repeated disconnects. Keep state semantics identical across transports. A UI component should consume normalized status events, not know whether they came from HTTP or a stream.&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="nx"&gt;statusStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="nx"&gt;uploadId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This also makes automated tests easier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Respect background behavior
&lt;/h2&gt;

&lt;p&gt;Browsers throttle timers and may suspend connections when the screen locks. Pause aggressive polling when hidden, but perform an immediate status fetch on visibility change. Do not display a failure merely because no event arrived while suspended.&lt;/p&gt;

&lt;h2&gt;
  
  
  Protect authorization
&lt;/h2&gt;

&lt;p&gt;A status channel can leak filenames, processing details, or other guests' uploads. Authenticate every subscription, scope it to allowed IDs, and avoid putting access tokens in URLs that appear in logs. Recheck authorization during long-lived connections.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measure the right outcome
&lt;/h2&gt;

&lt;p&gt;Compare time from server state change to visible UI update, reconnect frequency, requests per completed upload, and battery/network cost. A persistent connection with constant failures is not more “real time” than two-second polling.&lt;/p&gt;

&lt;p&gt;I help build Gathmo and have a commercial interest in upload UX. The &lt;a href="https://gathmo.com/how-it-works" rel="noopener noreferrer"&gt;Gathmo browser workflow&lt;/a&gt; is one example where processing status matters; the transport tradeoffs are platform-neutral.&lt;/p&gt;

&lt;p&gt;For most guest upload pages, reliable polling is the reference implementation. Add SSE when it measurably improves latency or load, and choose WebSockets only when bidirectional behavior justifies the operational surface.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>programming</category>
    </item>
    <item>
      <title>Designing Upload Expiration as a Recoverable State</title>
      <dc:creator>Gathmo</dc:creator>
      <pubDate>Fri, 17 Jul 2026 18:46:49 +0000</pubDate>
      <link>https://dev.to/gathmo/designing-upload-expiration-as-a-recoverable-state-1c8e</link>
      <guid>https://dev.to/gathmo/designing-upload-expiration-as-a-recoverable-state-1c8e</guid>
      <description>&lt;p&gt;Signed upload sessions expire. Event contribution windows close. Storage reservations are reclaimed. These are normal lifecycle events, but many interfaces reduce all of them to “Upload failed.” A better protocol makes expiration explicit and recoverable where policy allows it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Distinguish three clocks
&lt;/h2&gt;

&lt;p&gt;An upload workflow usually has at least three deadlines:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;the event contribution window;&lt;/li&gt;
&lt;li&gt;the server-side upload intent expiry;&lt;/li&gt;
&lt;li&gt;the short-lived storage credential expiry.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;They should not share one timestamp. The event may accept contributions until midnight while a credential lasts five minutes and an intent can be renewed for an hour.&lt;/p&gt;

&lt;p&gt;Return the clocks in the session response with server time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"serverNow"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"2026-07-17T18:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"eventClosesAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"2026-07-18T00:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"intentExpiresAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"2026-07-17T19:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"credentialExpiresAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"2026-07-17T18:05:00Z"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The client can display useful warnings without trusting its own clock for authorization.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model expiration by state
&lt;/h2&gt;

&lt;p&gt;A credential expiring during transfer is different from an intent expiring before completion. Use stable reasons:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;credential_expired -&amp;gt; request renewal
intent_expired -&amp;gt; reconcile committed parts, then renew or restart
event_closed -&amp;gt; stop new work, preserve local recovery guidance
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not automatically restart from byte zero. First ask the control plane which parts were committed and whether the original object key remains valid.&lt;/p&gt;

&lt;h2&gt;
  
  
  Renew narrowly
&lt;/h2&gt;

&lt;p&gt;A renewal endpoint should accept the upload ID and prove continuity with the guest session. It rechecks event state, file policy, rate limits, and committed size. The response returns a new credential for the same object.&lt;/p&gt;

&lt;p&gt;Do not let the browser extend an intent indefinitely. Define a maximum session age and a bounded number of renewals. Long uploads may receive a larger initial policy based on file size and observed throughput.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handle the closing boundary fairly
&lt;/h2&gt;

&lt;p&gt;Decide what happens to an upload already transferring when the event window closes. Reasonable policies include a grace period for active sessions or a strict cutoff for sensitive events. Document the choice and apply it on the server.&lt;/p&gt;

&lt;p&gt;The UI should say “The upload window closed while this file was transferring” rather than suggesting a network error. If local retry later is impossible, tell the guest to keep the original and contact the host.&lt;/p&gt;

&lt;h2&gt;
  
  
  Avoid timer-driven corruption
&lt;/h2&gt;

&lt;p&gt;Client timers are advisory. Background tabs throttle JavaScript, devices sleep, and clocks change. Never delete server state merely because a browser timer fired. Each request is authorized against server time.&lt;/p&gt;

&lt;p&gt;When the page returns from the background, refresh status before continuing. The last part may have completed even though its response never reached the page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preserve user intent
&lt;/h2&gt;

&lt;p&gt;Keep safe local metadata about files that need attention: upload ID, event ID, filename, size, and last known committed parts. Do not persist storage credentials. If the File object cannot survive a restart, ask the user to reselect the same file and verify basic attributes before resuming.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test every boundary
&lt;/h2&gt;

&lt;p&gt;Use fake clocks to test expiry immediately before a part begins, during a response timeout, after all parts but before completion, and while the page is hidden. Verify the displayed message and final server state.&lt;/p&gt;

&lt;p&gt;I help build Gathmo and have a commercial interest in reliable event uploads. The &lt;a href="https://gathmo.com/help" rel="noopener noreferrer"&gt;Gathmo help centre&lt;/a&gt; provides product-specific recovery guidance; the expiration model here can be used in any resumable upload system.&lt;/p&gt;

&lt;p&gt;Expiration is not an exceptional crash. It is a state transition with policy, evidence, and a recovery path.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>ux</category>
    </item>
    <item>
      <title>Integrity Manifests for Event Album Exports</title>
      <dc:creator>Gathmo</dc:creator>
      <pubDate>Fri, 17 Jul 2026 18:42:46 +0000</pubDate>
      <link>https://dev.to/gathmo/integrity-manifests-for-event-album-exports-1hp1</link>
      <guid>https://dev.to/gathmo/integrity-manifests-for-event-album-exports-1hp1</guid>
      <description>&lt;p&gt;An event album export is usually treated as a zip file and a success toast. That is not enough for a handover containing hundreds of photographs, videos, and voice messages. The recipient needs evidence that the package is complete, readable, and tied to a specific export request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define the export boundary
&lt;/h2&gt;

&lt;p&gt;Capture the event identifier, export request ID, creation time, selection rule, media states included, and requester role. An export of “approved media only” is different from an administrative archive containing removed or pending items.&lt;/p&gt;

&lt;p&gt;Freeze the selection at a consistent point. If uploads continue while the archive is built, either use a database snapshot or state clearly which cutoff timestamp applies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create a machine-readable manifest
&lt;/h2&gt;

&lt;p&gt;Include one entry per file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"photos/IMG_1042.jpg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"mediaId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"m_8f2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"bytes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;4821931&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"sha256"&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="nl"&gt;"uploadedAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"2026-07-17T12:10:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"state"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"approved"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use stable internal media IDs, not contributor contact details. Add the original filename only if the product promises to preserve it and the recipient needs it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hash the bytes you deliver
&lt;/h2&gt;

&lt;p&gt;Compute the checksum from the exact exported object, after any transformation. If the archive contains an original and a display derivative, list both separately. A hash stored only in an internal database does not prove the downloaded bytes match.&lt;/p&gt;

&lt;p&gt;For very large exports, stream each object through the hash function while writing the archive. Avoid loading files into application memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make partial failure visible
&lt;/h2&gt;

&lt;p&gt;Do not quietly skip an unavailable object. Mark the export failed or include an explicit error section that prevents the UI from claiming completeness. Retry transient storage reads with bounded backoff, then preserve the failure reason for the operator.&lt;/p&gt;

&lt;p&gt;The archive builder should be idempotent. Repeating the same request may create a new package, but its selection and manifest should remain comparable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sign or anchor the manifest
&lt;/h2&gt;

&lt;p&gt;A server-side signature over the canonical manifest can show that it was produced by the service and not modified during handover. Key rotation requires a key identifier and a public verification procedure. For lower-risk use, storing the manifest hash in the audit log may be sufficient.&lt;/p&gt;

&lt;p&gt;Do not oversell a checksum. It proves byte integrity, not consent, authorship, or legal ownership.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify before announcing success
&lt;/h2&gt;

&lt;p&gt;After upload to the delivery location, read the archive footer, parse the manifest, and open a sample from every media type. Compare file count and total bytes with the frozen selection. Only then mark the export ready.&lt;/p&gt;

&lt;p&gt;Use a short-lived download link scoped to one package. Export authority should not grant album administration or access to future packages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Help the recipient verify
&lt;/h2&gt;

&lt;p&gt;Include a small README with the event label, cutoff, file count, total size, manifest format, and checksum command. Nontechnical recipients still benefit from a clear statement of what is present and what was excluded.&lt;/p&gt;

&lt;p&gt;I help build Gathmo and have a commercial interest in reliable media handovers. The &lt;a href="https://gathmo.com/help" rel="noopener noreferrer"&gt;Gathmo help centre&lt;/a&gt; provides product-specific guidance; the manifest approach works for any service that exports user media.&lt;/p&gt;

&lt;p&gt;A trustworthy export is not merely downloadable. It is defined, reproducible, checked, and accompanied by evidence that the bytes handed over are the bytes the system selected.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>security</category>
    </item>
    <item>
      <title>Threat Modeling Share Links for Private Event Albums</title>
      <dc:creator>Gathmo</dc:creator>
      <pubDate>Fri, 17 Jul 2026 18:42:28 +0000</pubDate>
      <link>https://dev.to/gathmo/threat-modeling-share-links-for-private-event-albums-fj4</link>
      <guid>https://dev.to/gathmo/threat-modeling-share-links-for-private-event-albums-fj4</guid>
      <description>&lt;p&gt;A private event album often uses a share link because guests will not create accounts. That improves participation and moves the security boundary into the URL. The design must assume links are copied into group chats, photographed from signs, saved by browser history, and opened on shared devices.&lt;/p&gt;

&lt;h2&gt;
  
  
  Identify the assets and actors
&lt;/h2&gt;

&lt;p&gt;Assets include original media, captions, contributor names, voice messages, moderation state, and export archives. Actors include invited guests, the host, suppliers, accidental recipients, automated scanners, and a person who receives an old device.&lt;/p&gt;

&lt;p&gt;“Anyone with the link” is not an actor definition. It is an authorization rule with predictable leakage paths.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate upload and viewing authority
&lt;/h2&gt;

&lt;p&gt;A QR code displayed in a venue may need to permit contribution without granting access to every existing file. Use distinct capabilities for upload, gallery viewing, moderation, and export. If one link does everything, a photograph of the sign becomes an administrator credential.&lt;/p&gt;

&lt;p&gt;Capability tokens should be random, long enough to resist guessing, and stored hashed where practical. Avoid meaningful event names or sequential IDs as the secret part of the URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Plan for referrer leakage
&lt;/h2&gt;

&lt;p&gt;If an album page loads third-party images, analytics, or outbound links, the full URL can appear in a Referer header unless policy prevents it. Set a restrictive referrer policy and keep capability tokens out of query strings used by third-party scripts.&lt;/p&gt;

&lt;p&gt;Do not place secrets in page titles, Open Graph metadata, or client-side error reports. Messaging previews may fetch and cache those fields automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat scanners as normal traffic
&lt;/h2&gt;

&lt;p&gt;Email security products and chat applications may open links before a person does. A GET request must not consume a one-time capability, approve an action, or close an event. State changes require an explicit, protected request.&lt;/p&gt;

&lt;p&gt;Rate limiting should tolerate harmless preview fetches while still detecting enumeration and high-volume scraping.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add host-controlled rotation
&lt;/h2&gt;

&lt;p&gt;The host needs to rotate a leaked share link without deleting the album. New tokens should become active immediately, and old ones should expire after a deliberate overlap or at once for an incident.&lt;/p&gt;

&lt;p&gt;Record which printed materials and scheduled messages contain the old code. Rotation without an inventory can lock out legitimate guests or leave the compromised route in circulation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reduce exposure on shared devices
&lt;/h2&gt;

&lt;p&gt;Avoid long-lived authentication in local storage for public guest links. Offer a clear sign-out or “forget this album” action when viewing authority is stronger than upload authority. Sensitive albums may require an additional event code that is not printed next to the QR symbol.&lt;/p&gt;

&lt;p&gt;Browser history is difficult to control. Keep route names neutral and avoid rendering sensitive thumbnails before access is established.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model closure and deletion
&lt;/h2&gt;

&lt;p&gt;An event end date should change authorization behavior. Upload authority can close while view access remains for review. Export links should be short-lived and independently revocable. Deleted media should disappear from derivatives and caches, not only the database row.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the uncomfortable paths
&lt;/h2&gt;

&lt;p&gt;Copy the link into a different browser profile, open it through a messaging preview, inspect network referrers, attempt old tokens after rotation, and load cached image URLs after removal. A threat model becomes useful when it produces tests.&lt;/p&gt;

&lt;p&gt;I help build Gathmo and have a commercial interest in this area. The &lt;a href="https://gathmo.com/about" rel="noopener noreferrer"&gt;Gathmo product principles&lt;/a&gt; provide context for our approach, but the share-link threats described here are common to many private collaboration tools.&lt;/p&gt;

&lt;p&gt;A share link is a credential. Design its scope, lifetime, leakage controls, rotation, and evidence with the same care as any other authorization mechanism.&lt;/p&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Privacy-Preserving Telemetry for Guest Upload Pipelines</title>
      <dc:creator>Gathmo</dc:creator>
      <pubDate>Fri, 17 Jul 2026 18:42:02 +0000</pubDate>
      <link>https://dev.to/gathmo/privacy-preserving-telemetry-for-guest-upload-pipelines-209l</link>
      <guid>https://dev.to/gathmo/privacy-preserving-telemetry-for-guest-upload-pipelines-209l</guid>
      <description>&lt;p&gt;An upload system needs observability. Operators must know whether failures happen during selection, transfer, acceptance, or processing. The easy implementation sends every filename, event identifier, IP address, browser fingerprint, and retry to an analytics vendor. That produces a detailed dashboard and an unnecessary privacy liability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Begin with operational questions
&lt;/h2&gt;

&lt;p&gt;Write the questions before choosing events:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What percentage of upload intents become accepted files?&lt;/li&gt;
&lt;li&gt;Where does time accumulate?&lt;/li&gt;
&lt;li&gt;Which coarse browser families fail more often?&lt;/li&gt;
&lt;li&gt;How many bytes are retransmitted?&lt;/li&gt;
&lt;li&gt;Are processing failures isolated to one media type?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these requires a persistent identity that follows a guest across events.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use an event-local session
&lt;/h2&gt;

&lt;p&gt;Generate a random telemetry session for the current event and browser visit. Rotate it when the upload page is reopened after a reasonable interval. Do not join it to marketing cookies or another event.&lt;/p&gt;

&lt;p&gt;An upload ID may appear in both control-plane logs and client events, but it should be random and scoped. Store the mapping to the actual object only where operations require it, with stricter access and retention.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define a small event vocabulary
&lt;/h2&gt;

&lt;p&gt;Avoid arbitrary log messages. Use structured stages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"stage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"part_transfer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"timeout"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"attempt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"sizeBucket"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"5-10MB"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"network"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"cellular"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bucket sizes, durations, and network types. Exact values can make rare combinations identifying and rarely improve a reliability decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep content out of telemetry
&lt;/h2&gt;

&lt;p&gt;Do not send filenames, captions, album names, raw error bodies, signed URLs, or media metadata by default. Error objects often contain request URLs with credentials. Build an allowlist serializer instead of spreading the browser error into an analytics payload.&lt;/p&gt;

&lt;p&gt;Server logs should redact authorization headers and query signatures before storage. Confirm redaction with automated tests, not only configuration promises.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate product analytics and incident evidence
&lt;/h2&gt;

&lt;p&gt;Aggregate funnel metrics can use short retention and coarse dimensions. Security or abuse investigations may justify more detailed evidence for a limited case. Keep those systems separate, require an incident reason, and log access.&lt;/p&gt;

&lt;p&gt;A single enormous analytics table tends to retain the most sensitive field for the longest policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design deletion and expiry
&lt;/h2&gt;

&lt;p&gt;Set retention per data class. Raw client events may need days, aggregated reliability metrics months, and security evidence a case-specific schedule. Expiry should be enforced by storage lifecycle rules and tested with sample records.&lt;/p&gt;

&lt;p&gt;If an event owner requests deletion, know which operational records can be removed and which minimal security records must remain. Document this distinction in plain language.&lt;/p&gt;

&lt;h2&gt;
  
  
  Detect failure without fingerprinting
&lt;/h2&gt;

&lt;p&gt;Browser family, major version, coarse device class, and feature support are usually enough. Prefer capability flags such as &lt;code&gt;supportsCreateImageBitmap&lt;/code&gt; or &lt;code&gt;supportsReadableStreamUpload&lt;/code&gt; over a high-entropy fingerprint.&lt;/p&gt;

&lt;p&gt;Sample successful flows and retain all rare failure categories only after removing sensitive fields. This reduces cost and limits unnecessary collection.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make dashboards honest
&lt;/h2&gt;

&lt;p&gt;An accepted upload is not a ready asset. Show separate rates for server acceptance and successful processing. Distinguish user cancellation from technical failure. Record when the page disappears without inferring that the guest intentionally abandoned the contribution.&lt;/p&gt;

&lt;p&gt;I help build Gathmo and have a commercial interest in privacy-aware event systems. The &lt;a href="https://gathmo.com/about" rel="noopener noreferrer"&gt;Gathmo company overview&lt;/a&gt; provides product context, while this telemetry model can be applied to other upload services.&lt;/p&gt;

&lt;p&gt;Observability is strongest when every field answers a real operational question. If a value is merely interesting, remove it. Smaller telemetry is easier to secure, easier to explain, and often easier to use.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>security</category>
      <category>privacy</category>
    </item>
    <item>
      <title>Memory-Safe Media Preflight in Mobile Browsers</title>
      <dc:creator>Gathmo</dc:creator>
      <pubDate>Fri, 17 Jul 2026 18:41:30 +0000</pubDate>
      <link>https://dev.to/gathmo/memory-safe-media-preflight-in-mobile-browsers-204c</link>
      <guid>https://dev.to/gathmo/memory-safe-media-preflight-in-mobile-browsers-204c</guid>
      <description>&lt;p&gt;Client-side media preflight can improve an upload experience, but it can also crash the page before the network request begins. A twelve-megapixel JPEG may be only four megabytes on disk and tens of megabytes after decoding. Creating several full-size canvases at once is enough to exhaust memory on older phones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preflight is policy, not editing
&lt;/h2&gt;

&lt;p&gt;Decide what the browser must prove before transfer. Useful checks include file count, compressed byte size, declared type, readable dimensions, and a conservative duration limit for video. Avoid mandatory re-encoding unless the product truly needs it. Every transformation adds CPU time, memory pressure, battery use, and another failure mode.&lt;/p&gt;

&lt;h2&gt;
  
  
  Process one file at a time
&lt;/h2&gt;

&lt;p&gt;A file picker may return twenty items. Do not decode all of them to build previews. Maintain a queue with one active decode on constrained devices and at most two on stronger ones.&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;for &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;file&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;files&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;inspect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;renderResult&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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;yieldToMainThread&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 UI can list filenames immediately while dimensions and thumbnails arrive progressively.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prefer metadata over full pixels
&lt;/h2&gt;

&lt;p&gt;Use &lt;code&gt;createImageBitmap&lt;/code&gt; with resize hints when supported. It can decode away from the main rendering path and avoid a full-resolution canvas for a small preview.&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;bitmap&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;createImageBitmap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;resizeWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;640&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;resizeQuality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;medium&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Always close bitmaps after drawing. Revoke object URLs when the component unmounts. Small leaks become large when guests select and remove files repeatedly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handle orientation and color carefully
&lt;/h2&gt;

&lt;p&gt;Modern browsers generally honor EXIF orientation when decoding, but behavior differs across APIs and older engines. Test portrait photographs from real iOS and Android devices. Do not strip metadata silently if the original is supposed to remain untouched; upload the source file and treat the preview as disposable UI.&lt;/p&gt;

&lt;p&gt;Color differences between the preview and exported original are usually less harmful than a destructive client-side conversion. Be explicit about whether the product preserves originals.&lt;/p&gt;

&lt;h2&gt;
  
  
  Video needs different limits
&lt;/h2&gt;

&lt;p&gt;Reading a complete video into an ArrayBuffer simply to inspect duration defeats streaming. Create a temporary media element from an object URL, wait for metadata, then release it. Add a timeout because damaged files may never emit the expected event.&lt;/p&gt;

&lt;p&gt;Do not generate multiple video thumbnails concurrently on mobile. Seek operations can trigger additional decoding and large temporary buffers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Survive cancellation
&lt;/h2&gt;

&lt;p&gt;Every preflight task needs an abort path when the user removes a file, picks a replacement, navigates away, or the event closes. Use an AbortController for your own async boundaries and check the signal between decode stages.&lt;/p&gt;

&lt;p&gt;Report errors per file. One unreadable image should not discard nineteen valid contributions. Use specific messages: unsupported type, file too large, video too long, or preview unavailable but upload permitted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measure without collecting filenames
&lt;/h2&gt;

&lt;p&gt;Track coarse outcomes such as decoded, rejected by size, preview timeout, and out-of-memory page recovery. Filenames and media contents are unnecessary for operational metrics.&lt;/p&gt;

&lt;p&gt;I help build Gathmo and therefore have a commercial interest in reliable browser media handling. The &lt;a href="https://gathmo.com/help" rel="noopener noreferrer"&gt;Gathmo help centre&lt;/a&gt; describes product-specific behavior; the memory rules here apply to any mobile upload interface.&lt;/p&gt;

&lt;p&gt;A good preflight pipeline is intentionally modest. It validates what the server needs, produces a small preview, releases resources promptly, and never risks the original merely to make the interface look faster.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
