<?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>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>
    <item>
      <title>A moderation architecture that never blocks user uploads</title>
      <dc:creator>Gathmo</dc:creator>
      <pubDate>Wed, 15 Jul 2026 16:33:48 +0000</pubDate>
      <link>https://dev.to/gathmo/a-moderation-architecture-that-never-blocks-user-uploads-3db0</link>
      <guid>https://dev.to/gathmo/a-moderation-architecture-that-never-blocks-user-uploads-3db0</guid>
      <description>&lt;p&gt;User-generated media systems often make one state do too much. An item is either “uploaded” or “not uploaded,” while storage, processing, moderation, and public visibility are treated as implementation details.&lt;/p&gt;

&lt;p&gt;That model breaks down during live events. A guest needs immediate confirmation that a contribution arrived, but an organizer may require review before the item reaches a shared album or display. Moderation can slow down without turning successful uploads into apparent failures.&lt;/p&gt;

&lt;p&gt;The solution is to separate ingestion from publication.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use an append-first ingestion path
&lt;/h2&gt;

&lt;p&gt;The synchronous request should do only the work needed to accept the contribution safely:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;authenticate or validate the temporary contributor session;&lt;/li&gt;
&lt;li&gt;confirm the uploaded object and basic policy constraints;&lt;/li&gt;
&lt;li&gt;create a canonical media record;&lt;/li&gt;
&lt;li&gt;assign an initial moderation state; and&lt;/li&gt;
&lt;li&gt;enqueue asynchronous processing.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The response can then return a durable identifier and a truthful state such as &lt;code&gt;received&lt;/code&gt; or &lt;code&gt;pending_review&lt;/code&gt;.&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;MediaRecord&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;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;ingestionState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;received&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;processing&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;ready&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;failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&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="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;approved&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;hidden&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;removed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;visibilityState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;private&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;album&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;live_wall&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;These dimensions should not be collapsed into one enum. A file can be technically ready, pending moderation, and private at the same time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the live wall out of the write path
&lt;/h2&gt;

&lt;p&gt;The display should query or subscribe only to an approved projection. It should never consume the raw ingestion queue.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;guest upload -&amp;gt; canonical media record -&amp;gt; processing queue
                                      -&amp;gt; moderation queue

approved projection -&amp;gt; album API
                    -&amp;gt; live wall feed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If moderation falls behind, the display can continue rotating previously approved media. Guests can continue uploading, and moderators can catch up without back-pressure reaching the upload endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make moderation transitions explicit
&lt;/h2&gt;

&lt;p&gt;Each action should be a named state transition with an actor and timestamp. Useful events include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;media_received&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;processing_completed&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;moderation_approved&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;moderation_hidden&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;visibility_added_to_wall&lt;/code&gt;; and&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;removal_requested&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An audit trail makes concurrent moderator behavior understandable and supports undo. “Hidden” should usually be reversible; deletion belongs to a separate workflow with its own confirmation and retention rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design the moderator queue for decisions, not browsing
&lt;/h2&gt;

&lt;p&gt;A good queue puts decision context next to the media:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;event and contribution time;&lt;/li&gt;
&lt;li&gt;media type and duration;&lt;/li&gt;
&lt;li&gt;processing or safety warnings;&lt;/li&gt;
&lt;li&gt;current album and live-wall visibility;&lt;/li&gt;
&lt;li&gt;previous moderator actions; and&lt;/li&gt;
&lt;li&gt;keyboard-accessible approve and hide controls.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Newest-first ordering is not always best. During a live event, the queue may prioritize items waiting longest, processing warnings, or media requested by the display operator. The ordering rule should be visible so moderators understand why an item appears next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prevent duplicate side effects
&lt;/h2&gt;

&lt;p&gt;Approval may trigger notifications, search indexing, derivative generation, or display updates. Moderation commands therefore need the same idempotency discipline as payment or upload confirmation endpoints.&lt;/p&gt;

&lt;p&gt;Use a command ID or state-version precondition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;approveMedia&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;mediaId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;expectedVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;commandId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cmd_01J...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If two moderators act on the same item, one transition succeeds and the other receives the current state instead of replaying every downstream effect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Publish from a stable read model
&lt;/h2&gt;

&lt;p&gt;Album and display clients should read a projection optimized for visibility, not join raw operational tables on every refresh. The projection can contain the approved derivative URL, aspect ratio, caption, approval time, and event sequence number.&lt;/p&gt;

&lt;p&gt;A monotonically increasing event sequence helps clients resume after disconnection without reloading the entire album. It also supports deterministic live-wall rotation when network conditions are poor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Communicate the right success state to contributors
&lt;/h2&gt;

&lt;p&gt;The contributor interface should say “Your upload was received” when the canonical record exists. If review is enabled, it can add “It will appear after host approval.”&lt;/p&gt;

&lt;p&gt;Do not keep a progress bar at 99% while a human moderator is deciding. Technical completion and editorial visibility operate on different clocks.&lt;/p&gt;

&lt;p&gt;This separation is central to the moderated event workflow described for &lt;a href="https://gathmo.com/corporate" rel="noopener noreferrer"&gt;Gathmo corporate events&lt;/a&gt;: guest contribution can remain fast even when publication is deliberately controlled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure drills worth running
&lt;/h2&gt;

&lt;p&gt;Test the architecture under operational pressure:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pause all moderation workers while uploads continue.&lt;/li&gt;
&lt;li&gt;Disconnect and reconnect the live wall.&lt;/li&gt;
&lt;li&gt;Approve the same item concurrently from two moderator sessions.&lt;/li&gt;
&lt;li&gt;Fail derivative generation after the canonical record is created.&lt;/li&gt;
&lt;li&gt;Hide an item that is currently visible on multiple displays.&lt;/li&gt;
&lt;li&gt;Restore a moderator session with stale queue data.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The goal is not instant publication at any cost. It is independent, observable progress: ingestion continues, moderation remains auditable, and every public surface shows only the state it is allowed to consume.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>architecture</category>
      <category>performance</category>
      <category>ux</category>
    </item>
    <item>
      <title>Designing a browser upload flow for unreliable event Wi-Fi</title>
      <dc:creator>Gathmo</dc:creator>
      <pubDate>Wed, 15 Jul 2026 15:51:11 +0000</pubDate>
      <link>https://dev.to/gathmo/designing-a-browser-upload-flow-for-unreliable-event-wi-fi-jeg</link>
      <guid>https://dev.to/gathmo/designing-a-browser-upload-flow-for-unreliable-event-wi-fi-jeg</guid>
      <description>&lt;p&gt;An upload form that works perfectly on office Wi-Fi can fail spectacularly at a wedding or conference. Hundreds of phones share one access point, mobile reception drops between rooms, and users close the browser as soon as they think the upload is done.&lt;/p&gt;

&lt;p&gt;The hard part is not accepting a file. It is designing a flow that remains understandable when every assumption about connectivity is wrong.&lt;/p&gt;

&lt;p&gt;This article describes the reliability model I use for browser-based event uploads: explicit client states, direct object-storage uploads, bounded retries, persistent intent, and confirmation that reflects server truth rather than UI optimism.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with a state machine, not a spinner
&lt;/h2&gt;

&lt;p&gt;A single &lt;code&gt;isUploading&lt;/code&gt; boolean hides the situations that matter most. A useful client model separates at least these states:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;selected&lt;/code&gt; — the browser has access to the local file.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;preparing&lt;/code&gt; — metadata validation, optional image resizing, and checksum work are running.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;authorizing&lt;/code&gt; — the client is requesting an upload URL or session from the API.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;transferring&lt;/code&gt; — bytes are moving to object storage.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;confirming&lt;/code&gt; — the application API is registering the completed object.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;complete&lt;/code&gt; — the server has acknowledged the media record.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;retryable_error&lt;/code&gt; — the operation can continue without selecting the file again.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;terminal_error&lt;/code&gt; — the file or policy is invalid and needs user action.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This distinction prevents one of the most damaging UX bugs: showing success after storage accepted the bytes but before the application created the corresponding database record.&lt;/p&gt;

&lt;p&gt;The UI should render from the state machine. “Uploading 63%” belongs to &lt;code&gt;transferring&lt;/code&gt;; “Finishing…” belongs to &lt;code&gt;confirming&lt;/code&gt;; “Added to the album” belongs only to &lt;code&gt;complete&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Upload media directly to object storage
&lt;/h2&gt;

&lt;p&gt;Routing large photos and videos through the application server adds an unnecessary bottleneck. A more resilient path is:&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: request upload authorization
API -&amp;gt; browser: signed URL + object key + constraints
browser -&amp;gt; object storage: upload bytes
browser -&amp;gt; API: confirm object key and metadata
API -&amp;gt; browser: canonical media record
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API remains responsible for authorization, quotas, file policy, ownership, and the final media record. It does not need to proxy every byte.&lt;/p&gt;

&lt;p&gt;Signed upload URLs should be short-lived, bound to one object key, and limited to the expected content type and size where the storage provider supports those constraints. Never let the browser choose an arbitrary bucket key that becomes trusted application data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make confirmation idempotent
&lt;/h2&gt;

&lt;p&gt;The final confirmation request is especially vulnerable. The storage upload may have succeeded, but the phone can lose connectivity before receiving the API response. If a retry creates a second record, the user sees duplicate media even though only one object exists.&lt;/p&gt;

&lt;p&gt;Give each upload attempt a stable client-generated ID. The confirmation endpoint should treat that ID as an idempotency key:&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;"uploadAttemptId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"01J..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"objectKey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"events/evt_123/uploads/01J....jpg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"originalName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"IMG_4821.jpg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"contentType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&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;span class="nl"&gt;"size"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3842017&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;If the same confirmation arrives twice, return the same media record. This turns an ambiguous network timeout into a safe retry.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retry deliberately
&lt;/h2&gt;

&lt;p&gt;Automatic retries are useful only when they are bounded and visible.&lt;/p&gt;

&lt;p&gt;For authorization and confirmation requests, exponential backoff with jitter is usually appropriate. For example: 500 ms, 1.5 s, 4 s, then stop and show a retry button. Do not create an infinite background loop on a guest’s phone.&lt;/p&gt;

&lt;p&gt;For the byte transfer, retry policy depends on file size and storage support. Restarting a 2 MB image is reasonable. Restarting a 300 MB video at 95% is not. Large media should use multipart or resumable uploads so the client can continue from completed chunks.&lt;/p&gt;

&lt;p&gt;Classify errors before retrying:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Retry: timeouts, connection resets, and most 5xx responses.&lt;/li&gt;
&lt;li&gt;Refresh authorization, then retry: expired signed URL.&lt;/li&gt;
&lt;li&gt;Do not retry automatically: unsupported type, quota exceeded, authentication failure, or a 413 size rejection.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The error message should say what happens to the selected file. “Connection lost — your photo is still selected” is much more useful than “Something went wrong.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Preserve upload intent across short disruptions
&lt;/h2&gt;

&lt;p&gt;Mobile browsers are aggressive about suspending pages. At minimum, persist lightweight upload metadata in IndexedDB: attempt ID, object key, state, filename, size, and timestamps.&lt;/p&gt;

&lt;p&gt;Persisting the actual file is more complicated. A &lt;code&gt;File&lt;/code&gt; object cannot always be recovered after a browser restart, and large Blobs consume meaningful storage. Use this as a progressive enhancement rather than promising offline uploads everywhere.&lt;/p&gt;

&lt;p&gt;Even without persisting the bytes, durable metadata helps reconcile uncertainty. On page return, the client can ask the API whether an attempt was confirmed and avoid presenting an already completed item as failed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat progress as an estimate
&lt;/h2&gt;

&lt;p&gt;Upload progress measures bytes sent by the client, not necessarily durable application completion. Reserve the final visual step for server confirmation.&lt;/p&gt;

&lt;p&gt;A practical progress mapping is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;0–10%: preparing and authorizing&lt;/li&gt;
&lt;li&gt;10–90%: byte transfer&lt;/li&gt;
&lt;li&gt;90–99%: confirmation and processing&lt;/li&gt;
&lt;li&gt;100%: canonical record returned&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is not mathematically pure, but it communicates the real workflow. A bar stuck at 100% while the UI still says “processing” feels broken; a bar at 95% with “Finishing…” is honest.&lt;/p&gt;

&lt;p&gt;For multiple files, show both per-item state and batch progress. One failed video should not make twelve successful photos look unsuccessful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validate twice
&lt;/h2&gt;

&lt;p&gt;Client validation improves feedback, but server validation is authoritative. Check size, type, count, and basic media constraints before authorization, then verify them again during confirmation or asynchronous processing.&lt;/p&gt;

&lt;p&gt;Do not trust a filename extension or browser-provided MIME type. Inspect file signatures server-side, isolate untrusted media, and generate derivatives in a controlled worker process.&lt;/p&gt;

&lt;p&gt;For event products, moderation is a separate state from upload success. A guest should see that their file arrived even if it is waiting for host approval. Conflating “not publicly visible” with “upload failed” causes repeated submissions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design for abandonment
&lt;/h2&gt;

&lt;p&gt;Most guests are not committed users. They scanned a QR code, chose three photos, and expect to return to the party. Reliability therefore includes reducing the time during which the page requires attention.&lt;/p&gt;

&lt;p&gt;Keep the capture route free of account creation, defer nonessential work, and give a clear completion signal. If a transfer can continue safely while the user browses within the page, say so. If closing the tab will cancel it, say that instead.&lt;/p&gt;

&lt;p&gt;This constraint shaped the upload flow we use at &lt;a href="https://gathmo.com/" rel="noopener noreferrer"&gt;Gathmo&lt;/a&gt;, where guests contribute event photos, videos, and voice messages directly from a mobile browser. The product context is specific, but the engineering lesson generalizes: the network is not a reliable boundary, so every transition needs an observable and recoverable state.&lt;/p&gt;

&lt;h2&gt;
  
  
  A compact reliability checklist
&lt;/h2&gt;

&lt;p&gt;Before shipping a mobile media flow, test these cases intentionally:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The signed URL expires before transfer starts.&lt;/li&gt;
&lt;li&gt;Connectivity disappears at 5%, 60%, and immediately after storage success.&lt;/li&gt;
&lt;li&gt;Confirmation is sent twice.&lt;/li&gt;
&lt;li&gt;The browser is backgrounded during transfer.&lt;/li&gt;
&lt;li&gt;One file in a multi-file batch is rejected.&lt;/li&gt;
&lt;li&gt;The user taps upload twice.&lt;/li&gt;
&lt;li&gt;A large video exceeds policy.&lt;/li&gt;
&lt;li&gt;The object exists but confirmation never completed.&lt;/li&gt;
&lt;li&gt;Confirmation exists but the client never received the response.&lt;/li&gt;
&lt;li&gt;Moderation delays public visibility.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The happy path proves that uploads work. These failure paths prove that the product can be trusted at the exact moment users have only one chance to contribute.&lt;/p&gt;

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