Every ShareNotes note can be open in many places at once: a teacher and a classroom, a phone and a laptop in a clipboard room, two colleagues on a call. The common case is one person writing and several people watching, and the design is built around that.
Writes go over HTTP, reads come over the socket
This is the one decision everything else follows from. When you stop typing for a second, your browser saves the whole note with an ordinary HTTP request. That request is where the rules live: the server checks the note exists, refuses it if it was a view-once note already read, checks the PIN if the note is locked, and only then writes it to Postgres.
Then, and only then, the server sends the stored version to everyone watching. The WebSocket connection is for receiving. A browser can tell the server which notes it wants to follow; it cannot use the socket to change what anyone else sees.
Rule of thumb: never let one client's message be the thing other clients render. Route every change through the same checked path that stores it, and broadcast what was stored.
Fan-out
The server is a single Node process using the ws library, mounted at
/ws on the same server as the API. It keeps a map from each open connection
to the set of notes it follows. When a save succeeds, it walks that map and sends the new
content to every connection subscribed to that note. For the sizes we see, that loop is
cheap β a note has a handful of viewers, not thousands.
Notes locked for viewing are the exception. A connection only receives their content if it supplied the right PIN when it subscribed, and that is checked at the moment each update is sent β so subscribing to a note's address before someone creates it with a lock gets you nothing.
The reader's side
When an update arrives, the page replaces the note β unless you are typing in it at that moment. Yanking text out from under someone's cursor is worse than a short delay, so a focused editor keeps what you are writing and the latest version is there when you stop.
Reconnecting
Phones sleep, Wi-Fi drops, laptops close. The server pings each connection every 30 seconds so dead ones are noticed. The browser reconnects on its own with exponential backoff β one second, then two, four and so on up to thirty, for up to ten tries β and on reconnect it re-subscribes to every note it was following, including re-sending the PIN for locked ones. You do not see any of it; the note just catches up.
Where the design stops
- Last save wins. Each save replaces the whole note. There is no operational transform or CRDT merging two people's keystrokes. If two people type in the same note at the same moment, the later save wins. For one writer and many readers β the case it is built for β that never comes up. For a shared document with several active writers, use a tool built for it.
- Whole-document updates. We send the full note on every save, not a diff. Notes are small enough that this is simpler and fast.
- One process. The subscription map lives in memory.
What we would do at ten times the traffic
The single-process map is the first thing to go. With several servers, a save on one
has to reach viewers connected to another. Postgres gives us a way, and the plumbing is
half built: the server already LISTENs on a note_updates
channel. Nothing sends to it yet, because one process does not need it. At scale, each
save would NOTIFY that channel with the note's address, and every process
would look the note up and fan it out to its own connections. After that would come diffs instead of whole notes, and only then β if
multi-writer editing ever became the main use β a real merge algorithm.
Classroom mode is this mechanism plus a PIN on the write path; we wrote up how that works for teachers. If you would rather drive notes from a script, the REST API creates them the same way.