all projects

case study 03

Ping

Live demo

Realtime chat: I built the client against someone else's API, then wrote my own.

Ping screenshot 1
Ping screenshot 2
Ping screenshot 3
Ping screenshot 4
Ping screenshot 5
Ping screenshot 6
01 / 06

Ping started as a client for a third-party chat API I did not control. I documented its contract and quirks: the send acknowledgement carries no message or id, the sender never receives its own message event, and order comes from timestamps. Then I wrote my own backend to the same contract and closed the delivery gaps.

  1. 01
    Additive changes behind a capability handshake, not a breaking v2. The server announces its features on connect (server:hello). The client turns on delivery mode only when it sees them, so it still works against the original hosted API, and old clients still work against the new server.
  2. 02
    Idempotent sends with a client-generated key. Each send carries a clientMessageId; a unique index on (sender, clientMessageId) makes a retry return the stored message instead of creating a duplicate. That also makes it safe to fall back to REST when a socket acknowledgement is lost.
  3. 03
    Server-assigned order, not clocks. Each conversation has a counter incremented atomically in MongoDB; every message gets a seq. Gaps are allowed (a lost race burns a number), so seq means order, never count.
  4. 04
    Replay instead of refetch. On reconnect the client sends the highest seq it holds per conversation and receives only what it missed.
  5. 05
    Query invalidation over optimistic updates (Kept from the client work.) Optimistic updates feel faster but show the user something the server may reject.
  1. 01
    Integration tests on in-memory MongoDB: a duplicate send (sequential or concurrent) stores one message; concurrent sends from two users get unique ordered seqs; a forced disconnect then reconnect and sync gives no gaps and no duplicates; pagination boundary cases; the backfill migration is idempotent.
  2. 02
    Full reasoning: docs/delivery.md. writeup
  1. 01
    Single server instance. Running two would need the Socket.io Redis adapter; ordering already lives in the database, so it would hold.
  2. 02
    A sender's other tabs don't receive their own messages live; they catch up on the next sync.
  3. 03
    No read or delivery receipts.
  4. 04
    Sign-in is a sandbox: any phone number, no verification.
  • Next.js
  • TypeScript
  • TanStack Query
  • Express
  • Socket.io
  • MongoDB

Built on Invariant, my design languageSet in Sora, Rethink Sans and Geist Mono