<?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: Ondravan</title>
    <description>The latest articles on DEV Community by Ondravan (@ondravan).</description>
    <link>https://dev.to/ondravan</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%2F4204073%2F85c6d194-b3ba-4082-bc73-3b2ca0e67efc.jpg</url>
      <title>DEV Community: Ondravan</title>
      <link>https://dev.to/ondravan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ondravan"/>
    <language>en</language>
    <item>
      <title>A live photo wall for parties: WebSockets as a doorbell, REST as the truth</title>
      <dc:creator>Ondravan</dc:creator>
      <pubDate>Sat, 24 Oct 2026 06:56:29 +0000</pubDate>
      <link>https://dev.to/ondravan/a-live-photo-wall-for-parties-websockets-as-a-doorbell-rest-as-the-truth-3n76</link>
      <guid>https://dev.to/ondravan/a-live-photo-wall-for-parties-websockets-as-a-doorbell-rest-as-the-truth-3n76</guid>
      <description>&lt;p&gt;At a wedding or a party, there is usually a TV or a projector doing nothing. We wanted it to show every guest's photos as they are taken. That is the "live slideshow wall" in Fondays, a shared photo album app for events, and it turned out to be a nice small exercise in keeping real-time features honest.&lt;/p&gt;

&lt;p&gt;Here are the rules we ended up with.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The socket is a doorbell, not the record
&lt;/h2&gt;

&lt;p&gt;The wall opens a WebSocket and gets a short message whenever a photo lands. The message is a notification. The photos themselves always come from the normal REST API.&lt;/p&gt;

&lt;p&gt;That one decision removes a whole class of bugs. If the socket drops for ten seconds, or a message is lost, the client refetches over REST and is correct again. Nothing has to be replayed, and a failed publish never fails the upload that caused it.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Fan out locally, widen with Redis
&lt;/h2&gt;

&lt;p&gt;We run more than one API replica, and the phone uploading a photo is rarely connected to the same replica as the TV. So every replica fans messages out to its own sockets, and one Redis pub/sub channel widens that across replicas. Each message carries the id of the node that sent it, so a replica drops its own echo.&lt;/p&gt;

&lt;p&gt;Redis is only a widener. If it is down, people on the same replica still get live updates, and everyone else catches up on the next refetch.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Bounded mailboxes that latch
&lt;/h2&gt;

&lt;p&gt;A projector left running in a corner is the slowest consumer you will ever have. Each socket gets a small bounded mailbox. If it fills up, we do not queue forever and we do not silently drop messages: the backlog is replaced by a single "resync" message, and nothing more is delivered until the client takes it and refetches.&lt;/p&gt;

&lt;p&gt;Without the latch, a slow client would apply a stale backlog on top of the fresh copy it just loaded.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The wall can only listen
&lt;/h2&gt;

&lt;p&gt;The wall's socket accepts nothing from the client except a keep-alive reply. A screen left running at a venue should not be a second, unaudited way to write into somebody's album.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Privacy is checked at both ends
&lt;/h2&gt;

&lt;p&gt;Fondays has a mode called the Dark Room: the album is sealed until a time the host picks, then everything appears at once. A two-metre screen is the loudest possible way to break that promise, so a sealed photo is filtered out where messages are published and again where the wall reads. Either check alone would do. Both together survive the day someone refactors one of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Re-implement auth carefully
&lt;/h2&gt;

&lt;p&gt;Browsers cannot set headers on a WebSocket, so the socket authenticates differently from the HTTP API. That second implementation is where rules get lost. We wrote down every rule the HTTP path enforces (disabled accounts, permission scoping) and tested the socket path against the same list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stack
&lt;/h2&gt;

&lt;p&gt;Flutter for the apps, FastAPI and Postgres (raw SQL, no ORM) for the API, Redis for pub/sub and rate limits, Cloudflare R2 for storage.&lt;/p&gt;

&lt;p&gt;If you are hosting something this season, Fondays is free for hosts until June 2027: &lt;a href="https://fondays.com/free?c=devto" rel="noopener noreferrer"&gt;fondays.com/free&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We would like to hear how you handle slow consumers in your own real-time features.&lt;/p&gt;

</description>
      <category>websockets</category>
      <category>redis</category>
      <category>python</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
