<?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>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>
    <item>
      <title>Short-Lived Upload Credentials Without Giving the Browser Too Much Power</title>
      <dc:creator>Gathmo</dc:creator>
      <pubDate>Fri, 17 Jul 2026 18:40:44 +0000</pubDate>
      <link>https://dev.to/gathmo/short-lived-upload-credentials-without-giving-the-browser-too-much-power-3dkm</link>
      <guid>https://dev.to/gathmo/short-lived-upload-credentials-without-giving-the-browser-too-much-power-3dkm</guid>
      <description>&lt;p&gt;A guest upload page sits in a hostile place: an unauthenticated browser, often opened from a photographed QR code, on a network the event organizer does not control. The browser needs enough authority to send one file to one event. It should not receive a reusable credential that can list albums, read other media, or write forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with an upload intent
&lt;/h2&gt;

&lt;p&gt;Create a server-side intent before issuing storage access. The request contains only information needed for policy checks:&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;"eventId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"evt_123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"uploadId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"client-uuid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"size"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;7340032&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"image/jpeg"&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 server verifies that the event accepts contributions, the type and size are allowed, and the rate limit has room. It then stores an intent with an expiry and expected object key. A retry with the same event and upload ID returns the same intent rather than allocating a second object.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scope the credential to one object
&lt;/h2&gt;

&lt;p&gt;A signed request should bind the HTTP method, object key, content length range, media type, and a short expiry. Do not sign a bucket prefix if the client only needs one object. Do not return cloud credentials that can be refreshed independently.&lt;/p&gt;

&lt;p&gt;Five minutes is often enough for a photograph but not for a large video on congested venue Wi-Fi. Instead of increasing every expiry, let the control API renew an active intent after checking how much progress was made. Renewal should not change the destination key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate bytes from acceptance
&lt;/h2&gt;

&lt;p&gt;A successful storage upload does not mean the application accepted the contribution. The browser should call a completion endpoint that verifies object existence, byte length, checksum when available, and intent state.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;created -&amp;gt; transferring -&amp;gt; stored -&amp;gt; accepted -&amp;gt; processing -&amp;gt; ready
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The completion call must be idempotent. If the response disappears during a network switch, the next call should return the already accepted state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prevent confused-deputy mistakes
&lt;/h2&gt;

&lt;p&gt;Never let the client choose an arbitrary final object key. Derive it from trusted event and upload identifiers. Normalize the declared type, but verify the actual file signature during processing. A file named &lt;code&gt;photo.jpg&lt;/code&gt; can still contain another format.&lt;/p&gt;

&lt;p&gt;Keep organizer operations on a separate authorization path. A guest token must not become valid for deletion, moderation, export, or album settings simply because those endpoints share a domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make abuse limits explicit
&lt;/h2&gt;

&lt;p&gt;Apply limits at intent creation and completion: requests per network range, active intents per event, total bytes, and file count. Return a stable reason such as &lt;code&gt;event_closed&lt;/code&gt;, &lt;code&gt;type_not_allowed&lt;/code&gt;, or &lt;code&gt;capacity_reached&lt;/code&gt; so the UI can explain what happened without revealing internal infrastructure.&lt;/p&gt;

&lt;p&gt;Log credential issuance by event and intent, not by a persistent cross-event guest identity. Retain enough evidence to investigate abuse while avoiding a new tracking system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rotate signing keys safely
&lt;/h2&gt;

&lt;p&gt;Include a key identifier and accept the previous verification key during a short overlap. Rotation should not invalidate every upload already in progress. Emergency revocation is different: the control plane may mark an event or intent closed even when a storage signature has not yet expired, and completion must respect that state.&lt;/p&gt;

&lt;p&gt;I help build Gathmo and have a commercial interest in this problem. The &lt;a href="https://gathmo.com/corporate" rel="noopener noreferrer"&gt;Gathmo corporate event workflow&lt;/a&gt; is one context where scoped browser uploads matter, but the credential design is platform-neutral.&lt;/p&gt;

&lt;p&gt;The core rule is simple: give the browser authority for one narrow action, for one narrow object, for one short period. Everything else belongs behind the application control plane.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>security</category>
      <category>javascript</category>
      <category>programming</category>
    </item>
    <item>
      <title>Resumable Browser Uploads for Crowded Event Networks</title>
      <dc:creator>Gathmo</dc:creator>
      <pubDate>Fri, 17 Jul 2026 18:35:04 +0000</pubDate>
      <link>https://dev.to/gathmo/resumable-browser-uploads-for-crowded-event-networks-40el</link>
      <guid>https://dev.to/gathmo/resumable-browser-uploads-for-crowded-event-networks-40el</guid>
      <description>&lt;p&gt;Event uploads fail differently from normal office uploads. A wedding guest may move between venue Wi-Fi and mobile data, lock the phone while a video is transferring, or close the browser as soon as the progress bar reaches 100%. Hundreds of devices can share one access point, and users rarely wait around to diagnose an error.&lt;/p&gt;

&lt;p&gt;The usual &lt;code&gt;POST&lt;/code&gt; request plus optimistic success toast is not enough. A reliable browser flow needs a small protocol that distinguishes local preparation, network transfer, server acceptance, media processing, and final availability.&lt;/p&gt;

&lt;p&gt;This article describes a platform-neutral design for that protocol.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five states users actually experience
&lt;/h2&gt;

&lt;p&gt;Model each file as a durable state machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;selected
  -&amp;gt; preparing
  -&amp;gt; transferring
  -&amp;gt; accepted
  -&amp;gt; processing
  -&amp;gt; ready
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add terminal or recoverable branches:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;preparing    -&amp;gt; rejected_local
transferring -&amp;gt; paused | retryable_error | expired
accepted     -&amp;gt; processing_error
processing   -&amp;gt; ready | processing_error
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key distinction is between &lt;code&gt;transferring&lt;/code&gt; and &lt;code&gt;accepted&lt;/code&gt;. The browser may have sent every byte while the server has not yet committed the upload. Showing “done” at that boundary creates the most frustrating failure: the guest deletes the original, but the organizer never receives it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give every file a client-generated identity
&lt;/h2&gt;

&lt;p&gt;Create an upload ID before the first network request. A UUID is sufficient when combined with the event identifier:&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;uploadId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&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;uploadIntent&lt;/span&gt; &lt;span class="o"&gt;=&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;eventId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;size&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="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;lastModified&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="nx"&gt;lastModified&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;Send that identity when creating the server-side upload session. If the browser retries after a timeout, the server returns the existing session instead of creating a duplicate.&lt;/p&gt;

&lt;p&gt;This is idempotency at the workflow level. A guest can tap “retry” without having to understand whether the first request reached the server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate the control plane from file bytes
&lt;/h2&gt;

&lt;p&gt;Use a small JSON API for session creation and status, then transfer bytes through a resumable object-storage protocol or chunk endpoint.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /events/:eventId/upload-sessions
PUT  /upload-sessions/:uploadId/parts/:partNumber
POST /upload-sessions/:uploadId/complete
GET  /upload-sessions/:uploadId/status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The create response should include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the canonical upload ID;&lt;/li&gt;
&lt;li&gt;part size;&lt;/li&gt;
&lt;li&gt;expiry time;&lt;/li&gt;
&lt;li&gt;already committed parts;&lt;/li&gt;
&lt;li&gt;limits and accepted media types;&lt;/li&gt;
&lt;li&gt;a short-lived upload credential;&lt;/li&gt;
&lt;li&gt;the status polling URL.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not expose permanent storage credentials or organizer privileges to a guest browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choose chunks for mobile recovery, not maximum throughput
&lt;/h2&gt;

&lt;p&gt;Very small parts add request overhead. Very large parts make every interruption expensive. For event photos and short videos, starting around 5–10 MB per part is often a sensible compromise, but the server should be able to adjust by file size and network observations.&lt;/p&gt;

&lt;p&gt;Limit concurrency on mobile devices:&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;maxConcurrentParts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;saveData&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="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;More parallel requests can reduce throughput on a congested access point. Two predictable streams are often better than eight competing streams.&lt;/p&gt;

&lt;h2&gt;
  
  
  Persist only metadata that is safe to persist
&lt;/h2&gt;

&lt;p&gt;Store the upload session metadata in IndexedDB so a page reload can recover state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;uploads&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;uploadId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;eventId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;size&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="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;committedParts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;expiresAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;transferring&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Browsers cannot always reopen the original &lt;code&gt;File&lt;/code&gt; object after a restart. The UI must say so honestly: “Select the same video to resume.” Compare name, size, modification time, and optionally a lightweight fingerprint before attaching the new file handle to the existing session.&lt;/p&gt;

&lt;p&gt;Do not persist private event administration links, access tokens with long lifetimes, or unnecessary media metadata.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat network changes as normal
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;navigator.onLine&lt;/code&gt; is only a hint. A device can be online while a captive portal blocks the upload host. Use request outcomes and timeouts as the source of truth.&lt;/p&gt;

&lt;p&gt;Pause exponential backoff when the page is hidden, but do not cancel a part that is already close to completion. Add jitter so hundreds of guests do not retry at the same instant after venue Wi-Fi recovers:&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;function&lt;/span&gt; &lt;span class="nf"&gt;retryDelay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attempt&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;base&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;30&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;_000&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;attempt&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;base&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="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&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="nx"&gt;base&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After switching networks, query committed parts before resuming. The last response may have been lost even though the server stored the part.&lt;/p&gt;

&lt;h2&gt;
  
  
  Confirm server acceptance explicitly
&lt;/h2&gt;

&lt;p&gt;The completion endpoint should verify the part set, expected byte count, content constraints, and checksum where available. It returns an &lt;code&gt;accepted&lt;/code&gt; state only after the durable object exists.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"uploadId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"…"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"state"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"accepted"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"receivedBytes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;48277123&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"processingStatusUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/upload-sessions/…/status"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The browser can now tell the guest that the original is safely received. Processing may still generate previews, normalize video, scan content, or place the item into moderation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design the confirmation screen for impatient humans
&lt;/h2&gt;

&lt;p&gt;A useful confirmation has three layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Received&lt;/strong&gt; — the server has the original.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Preparing&lt;/strong&gt; — previews or video processing are still running.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Available&lt;/strong&gt; — the organizer workflow can use the item.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Never use only color. Include text, an icon, and a stable upload ID that support can reference. Let the guest add another item without losing the completed receipt.&lt;/p&gt;

&lt;p&gt;In an app-free event workflow such as &lt;a href="https://gathmo.com/how-it-works" rel="noopener noreferrer"&gt;Gathmo's browser upload flow&lt;/a&gt;, that clarity matters because a guest may interact with the product only once. There is no installed app to send a later recovery notification.&lt;/p&gt;

&lt;h2&gt;
  
  
  Protect the event from abandoned sessions
&lt;/h2&gt;

&lt;p&gt;Upload sessions need expiry and cleanup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;short-lived credentials;&lt;/li&gt;
&lt;li&gt;a maximum number of active sessions per guest or network;&lt;/li&gt;
&lt;li&gt;server-side size and type validation;&lt;/li&gt;
&lt;li&gt;cleanup of incomplete multipart objects;&lt;/li&gt;
&lt;li&gt;rate limits that tolerate retries;&lt;/li&gt;
&lt;li&gt;an abuse path that does not block legitimate crowded-event traffic.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keep accepted originals separate from uncommitted parts. Cleanup jobs must never infer abandonment from a missing browser heartbeat after acceptance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Observability without tracking guests
&lt;/h2&gt;

&lt;p&gt;Operational metrics can remain privacy-preserving:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;sessions created;&lt;/li&gt;
&lt;li&gt;acceptance rate;&lt;/li&gt;
&lt;li&gt;median attempts per part;&lt;/li&gt;
&lt;li&gt;bytes retransmitted;&lt;/li&gt;
&lt;li&gt;time from selection to acceptance;&lt;/li&gt;
&lt;li&gt;time from acceptance to ready;&lt;/li&gt;
&lt;li&gt;failure reason by browser and coarse network type;&lt;/li&gt;
&lt;li&gt;duplicate intents merged by idempotency key.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Avoid storing full filenames, precise IP history, or persistent cross-event identifiers in analytics when aggregates are enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the failure boundaries
&lt;/h2&gt;

&lt;p&gt;The happy path is the least interesting test. Automate and rehearse:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;timeout after the server commits a part but before the browser receives the response;&lt;/li&gt;
&lt;li&gt;refresh midway through a transfer;&lt;/li&gt;
&lt;li&gt;captive portal redirect;&lt;/li&gt;
&lt;li&gt;Wi-Fi-to-mobile transition;&lt;/li&gt;
&lt;li&gt;expired upload credential;&lt;/li&gt;
&lt;li&gt;duplicate completion request;&lt;/li&gt;
&lt;li&gt;file re-selection with the wrong file;&lt;/li&gt;
&lt;li&gt;processing failure after acceptance;&lt;/li&gt;
&lt;li&gt;event upload window closing during transfer;&lt;/li&gt;
&lt;li&gt;browser closure immediately after the last progress event.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For every test, assert both server state and user-visible wording.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reliability contract
&lt;/h2&gt;

&lt;p&gt;A reliable event upload flow makes four promises it can prove:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Retry does not create accidental duplicates.&lt;/li&gt;
&lt;li&gt;“Received” means the server durably accepted the original.&lt;/li&gt;
&lt;li&gt;The guest can recover from common mobile-network interruptions.&lt;/li&gt;
&lt;li&gt;The organizer can distinguish transferring, accepted, processing, and ready media.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That contract is more valuable than a perfectly smooth progress animation. Event guests forgive a brief retry. They do not forgive a success message for a file that never arrived.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>programming</category>
      <category>ux</category>
    </item>
    <item>
      <title>Designing an idempotent confirmation protocol for browser uploads</title>
      <dc:creator>Gathmo</dc:creator>
      <pubDate>Wed, 15 Jul 2026 16:34:46 +0000</pubDate>
      <link>https://dev.to/gathmo/designing-an-idempotent-confirmation-protocol-for-browser-uploads-p9g</link>
      <guid>https://dev.to/gathmo/designing-an-idempotent-confirmation-protocol-for-browser-uploads-p9g</guid>
      <description>&lt;p&gt;A direct-to-object-storage upload removes the application server from the byte path, but it creates a subtle distributed-systems problem at the end of the flow.&lt;/p&gt;

&lt;p&gt;The browser can finish uploading the object, send a confirmation request to the API, and lose connectivity before receiving the response. From the user's perspective the result is ambiguous: did the server create the media record, or should the browser try again?&lt;/p&gt;

&lt;p&gt;If the confirmation endpoint is not idempotent, retrying can create duplicate records. If the client refuses to retry, a successfully stored object can remain invisible to the application. The protocol needs a stable identity that survives network uncertainty.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model the upload as two durable operations
&lt;/h2&gt;

&lt;p&gt;The first operation stores bytes. The second operation creates or returns the canonical application record.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;browser -&amp;gt; API: authorize attempt
API -&amp;gt; browser: attempt ID, object key, signed upload URL
browser -&amp;gt; storage: transfer bytes
browser -&amp;gt; API: confirm attempt ID and object metadata
API -&amp;gt; browser: canonical media record
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The attempt ID is generated once and reused for every confirmation retry. It must not be regenerated when a request times out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give the attempt a server-owned scope
&lt;/h2&gt;

&lt;p&gt;A random UUID is useful, but identity alone is not authorization. The authorization response should bind the attempt to the authenticated or temporary guest session, the target event, the expected object key, and relevant constraints such as maximum size or content family.&lt;/p&gt;

&lt;p&gt;A minimal record might contain:&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;UploadAttempt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;eventId&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;contributorSessionId&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;objectKey&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;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;authorized&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;confirmed&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;rejected&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;createdAt&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;confirmedMediaId&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;The confirmation endpoint looks up this record rather than trusting event or object identifiers supplied again by the browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make confirmation return the existing result
&lt;/h2&gt;

&lt;p&gt;The endpoint should behave like a create-or-return operation.&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;confirmUpload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attemptId&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;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ConfirmInput&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;database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;attempt&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;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uploadAttempts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lockForUpdate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attemptId&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;attempt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;confirmed&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;media&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;confirmedMediaId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;assertAttemptBelongsToSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attempt&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;storedObject&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;inspectStoredObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;objectKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;validateObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;storedObject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;media&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;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;media&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;eventId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;eventId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;objectKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;objectKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;storedObject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;contentType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;storedObject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contentType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;moderationState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pending&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uploadAttempts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;markConfirmed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;media&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;media&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 transaction and uniqueness constraints matter. Two confirmation requests can arrive concurrently after a client retry or double tap. A row lock or compare-and-set transition ensures that only one media record wins.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do not use the object key as the only idempotency key
&lt;/h2&gt;

&lt;p&gt;Object keys are storage addresses, not necessarily user-intent identifiers. A multipart retry, a replacement workflow, or an administrative recovery may reuse or transform storage paths.&lt;/p&gt;

&lt;p&gt;An explicit attempt record gives the application a place to store authorization context, expiry, confirmation state, and the canonical result. It also makes abandoned-object cleanup safer because the system can distinguish an authorized but unconfirmed object from an unrelated storage file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reconcile after the browser returns
&lt;/h2&gt;

&lt;p&gt;Persist lightweight attempt metadata in IndexedDB or another durable client store:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;attempt ID;&lt;/li&gt;
&lt;li&gt;object key;&lt;/li&gt;
&lt;li&gt;filename and size;&lt;/li&gt;
&lt;li&gt;last known client state; and&lt;/li&gt;
&lt;li&gt;timestamps.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On page restore, query the API for each non-terminal attempt. The server response should distinguish at least:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;authorized&lt;/code&gt;: storage or confirmation may still be incomplete;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;confirmed&lt;/code&gt;: return the canonical media record;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;expired&lt;/code&gt;: the client needs a new authorization; and&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rejected&lt;/code&gt;: show a terminal explanation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The browser should not infer failure merely because it never received the original response.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate upload success from moderation success
&lt;/h2&gt;

&lt;p&gt;Confirmation means the application accepted the media record. It does not mean the item is approved for public display. Returning a separate moderation state avoids the common UX bug where guests retry a successful upload because it has not appeared on a live wall.&lt;/p&gt;

&lt;p&gt;The same separation is useful in the browser-based event contribution flow used by &lt;a href="https://gathmo.com/how-it-works" rel="noopener noreferrer"&gt;Gathmo&lt;/a&gt;: transfer, canonical confirmation, and visibility are different states even when the happy path feels like one action.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the ambiguous moments
&lt;/h2&gt;

&lt;p&gt;The most valuable protocol tests deliberately interrupt the flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Drop the response after the server commits confirmation.&lt;/li&gt;
&lt;li&gt;Send the same confirmation twice concurrently.&lt;/li&gt;
&lt;li&gt;Retry after the signed upload URL expires but the object already exists.&lt;/li&gt;
&lt;li&gt;Confirm an object whose size or content type differs from authorization.&lt;/li&gt;
&lt;li&gt;Restore the browser after confirmation completed on another device.&lt;/li&gt;
&lt;li&gt;Run cleanup while a confirmation transaction is in flight.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A reliable upload protocol is not one that never encounters ambiguity. It is one that can answer the same question repeatedly and return the same durable result.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>backend</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
