For example, when the name of a mailbox changes on IMAP, and the change is synced to our servers, we broadcast a mailboxes-updated Pusher message to the concerned clients.

The clients react by issuing a GET /mailboxes request. The API serializes and returns all mailboxes the user has access to, thus updating the one that changed on IMAP.

For resources that don’t change often, this simplistic approach is fun to work with. You just need to broadcast a generic message that describes the changed resource to have all clients update themselves.

It’s not that simple

As good as this strategy is for resources that don’t change often, it would be catastrophic for endpoints with continuous changes like marking an email as read, archiving, or posting chat comments. It would be unbearable if each of these POST actions from one user resulted in 20 GET requests when 20 of this user’s coworkers are online. Plus, if each of those 20 users read and mark that conversation as read, we are potentially looking at 20 read actions * 20 users = 400 GET !

We can’t really rate limit these GET requests. Remember, we want a live app.

So to make things live without flooding our API with GET requests we extensively use another cool Pusher feature: peer-to-peer channels. Each client establishes a persistent connection with other online members of its organization through that organization’s P2P Pusher channel.

Each member maintains a P2P connection with the other members of his Missive organization

Every time a live action is triggered, like posting a comment, the client broadcasts the action to the related organization channel. Each listening client renders the new comment in the related conversation using just the P2P-broadcasted data. When user A posts a comment, user B instantly sees it without querying the API.

The broadcasted message also contains the action_id . The action_id is unique to each action. They are stored by each client that successfully processes the action (e.g. the new comment).

Now that all clients have instantly been updated, the comment needs to be persisted on the server. The client does a POST /comments request, appending the action_id to the payload.

Then the API persists the comment and broadcasts a conversations-updated message also including the client-provided action_id . Thus, each client receiving the conversations-updated message can test if it needs to do a GET /conversations by looking at the given action_id . If they already have the action_id in their cache, bingo! They don’t have to because they already processed the action.

There are few reasons why the API broadcasts a conversations-updated to everyone after the client has already broadcasted a peer-to-peer message. One of them being if a client has no access to the conversation yet, it needs to first fetch it from the API.

Privacy

Right now “some” of you might be thinking:

Aren’t you broadcasting all comments in a single shared P2P channel, how do you manage privacy and accesses?

Good question, it’s true that not everyone from an organization has access to all of the organization conversations. To provide that level of privacy while using the public organization channel, we encrypt the broadcasted data using a secret key unique to each conversation.