In a previous post, I described how Instawork doubled our web development productivity by abandoning React and embracing server-rendered pages enhanced with Intercooler.js. A standard React/Redux setup requires a lot of duplicated work; logic has to be written once on the server (as the source of truth), and a second time on the client (for local state management). By doing all UI rendering in our Django codebase, we eliminate the need to write any custom app logic in JS. Of course, pure server-rendering doesn’t deliver the smooth experience users expect from web apps. That’s where Intercooler.js comes in: by adding a few HTML attributes to our elements, we can support a wide range of interactions by swapping content on the page with AJAX. All rendering is still done server-side, but pages can come alive with interactions like infinite scrolling, tab switching, dynamic multi-step forms, etc.
I always assumed that Intercooler’s swapping mechanism wouldn’t work for some of the more complex UI interactions we planned to add to our app. In particular, I worried how we’d handle real-time features in our product, like chats or streaming notifications. Would we need to give up on the zero-JS dream to implement these features?
Server-sent events on frontend
Intercooler has built-in support for SSE that manages the EventSource interface automatically. The syntax looks like this:
<div ic-trigger-on=”sse:status-updated-123" ic-src=”/status/123">
This is just regular HTML with two special attributes:
- ic-sse-src specifies the endpoint to use when creating an EventSource. Since SSE is done over HTTP, the browse will send along the appropriate cookies for authentication.
- ic-trigger-on specifies the event that will trigger the content swap. The special “sse:” syntax specifies that the given event should trigger a swap.
Server-sent events on the backend
Of course, what I described is the easy part. Behind the scenes, we need to implement the long-lived SSE endpoint, as well as a mechanism to push relevant events to all clients. Web frameworks like Django don’t handle long-lived connections very well, so we looked for other options. The Django Channels project extends Django to handle asynchronous communication, but it doesn’t support SSE out of the box. We wanted something that supported SSE in a language & framework-agnostic way.
Enter Mercure. Mercure is an open-source project that provides scalable SSE communication using a centralized Hub server
- Clients connect to the long-lived SSE endpoint on the Hub server. Authorization is done using a JWT token. The token encodes the event topics and event targets for which the client has permission.
- The server sends events by making a simple POST request to the Hub server with a JWT token. The token encodes the event name, topic, and the targets that should receive the event.
- Upon receiving a POST request, the Mercure Hub will send the event to all of the open SSE connections that match the topic and targets.
We considered a few options and services for handling SSE connections, but ultimately decided on Mercure for a few reasons. As mentioned above, Mercure is built around SSE, so its language and framework agnostic. Any system that can encode JWT tokens and make HTTP requests can use Mercure. We also appreciate that Mercure is a small micro-service that runs alongside our servers, rather than as a proxy in front of our servers (the approach used by Pushpin). This means there’s less risk to our site if Mercure goes down: we lose real-time features, but the main site will continue to function. Finally, we’ve found it to be very stable and scalable. In our load tests Mercure didn’t break a sweat, even when running at 10x the loads we expect.
Our Django integration with Mercure took the form of a helper function for sending events, and a view mixin. The view mixin does two things:
- it sets a mercureAuthorization cookie used by the client to connect to the Hub.
- it adds the Hub URL (with subscribed topics) to the response’s context data, so that it can be rendered as the ic-sse-src attribute.
I’ve shared a Gist with a complete example of our helpers, mixins, views and templates.
It’s easy to think that features like real-time UIs require a heavy client-side framework. But thanks to Intercooler’s built-in support for SSE, and the simplicity of setting up and integrating Mercure, we’ve been able to stick to our “no JS” philosophy while delivering the features our users expect. We continue to be surprised by the capabilities of Django + Intercooler + Mercure, and adding new realtime features is now a snap.
Have you added real-time updating to your web app? Are you thinking about adding it in the future? We’d love to hear about your experience and solutions, or provide more insights into Django, Intercooler, or Mercure.