Simple Declarative Presence for Hotwire apps with AnyCable

If Margaret had a more transparent line of communication, “Are you there?” would have been an unnecessary question, and we’d never have gotten a classic book. Likewise, for application users, looking at the online status indicator should be enough. Now think about your own users: could they be having a similar sense of disconnect? If “yes” is a possible answer here, continue reading to learn how to seamlessly add presence tracking functionality, with just a handful of HTML, powered by Hotwire and AnyCable.
In this post, we’ll show you how to implement user presence tracking in a Hotwire application with minimal effort by using AnyCable and its new primitives.

Irina Nazarova CEO at Evil Martians
Presence tracking in a nutshell
So, what is presence tracking? We’re pretty sure you actually encounter this feature every day in your digital life. And here are some examples, just to name a few:
- In Slack or Discord, you can see green (or red) dots next to users who are currently active, helping you know who is available for a quick chat.
- On Google Docs, profile pictures (or anonymous animals) show exactly who is viewing a document in real-time.
- E-commerce platforms show “X people are viewing this item right now” to create a sense of urgency (though we doubt this is “true” presence tracking).
- With collaborative software, presence indicators show which team members are interacting with which parts of a system (e.g., form fields).
See? So, presence tracking has been around for quite a while (does anyone remember ICQ 😁?). In modern times, where people spend a lot of time in virtual worlds, every application could benefit from online awareness capabilities. HTML-first Hotwire applications are no exception to this.
Let’s show off how our (new)ish AnyCable Presence makes implementing such capabilities remarkably simple.
Introducing AnyCable Presence for Hotwire!
Let’s start with the most common use case: displaying a list of users currently viewing our blog post.
Let’s assume that you already have an AnyCable server (v1.6+) at hand and are using the @anycable/turbo-stream
adapter for Hotwire on the client-side (if not, you’ll find the instructions below). In this case, implementing this feature becomes as straightforward as dropping a <turbo-cable-presence-source>
tag on the page with the following contents:
<% # locals: (post:, user: current_user) %>
<turbo-cable-presence-source
signed-stream-name="<%= signed_stream_name([post, :presence]) %>"
presence-id="<%= dom_id(user, :presence) %>"
>
<div>
<h3>Online Users</h3>
<div id="<%= dom_id(post, :presence_list) %>">
<!-- Users will be appended here -->
</div>
</div>
<!-- Template for how each user should be rendered -->
<template>
<%= turbo_stream.append dom_id(post, :presence) do %>
<div id="<%= dom_id(user, :presence) %>">
<span class="user-avatar">👤</span>
<span class="user-name">@<%= user.username %></span>
</div>
<% end %>
</template>
</turbo-cable-presence-source>
Note that the snippet above assumes that we’re in the context of a Ruby on Rails application (though Hotwire and AnyCable could be used with any backend and pure HTML). Moving on, let’s dissect the code and highlight the most important bits.
<turbo-cable-presence-source>
acts as a container for your presence UI. It automatically connects to a WebSocket server (AnyCable) and subscribes to a named stream. The presence set is attached to the stream, and the stream is also used to consume presence events. The subscription mechanism is similar to the one provided by the built-in Hotwire Rails <turbo-cable-stream-source>
element.
<template>
contains the presence information for the current user represented as a Turbo Stream action: whenever a user joins the stream, the action is triggered. You can have multiple stream actions, if you want.
Additionally note that we also provide the presence-id
attribute—it’s a unique presence action identifier within the given stream. It’s used by the server to de-duplicate sessions from the same user: the join action is only triggered when the user first enters the set (e.g., opens the page), not from a second browser tab. Similarly, the leave action is triggered only when all user’s sessions are completed. On leave, we remove the DOM element with the corresponding presence ID.
There are also a couple more features provided by the cable presence element out of the box, so let’s discuss those next.
While exploring this feature, we realized that most presence tracking UIs also show the number of online users. So, how can we implement this in a “Hotwire” way? We played around with several ideas and decided to go with an HTML attribute approach, as follows:
<turbo-cable-presence-source
signed-stream-name="<%= signed_stream_name([post, :presence]) %>"
presence-id="<%= dom_id(user, :presence) %>"
>
<div>
<h3>Online Users: <span data-presence-counter>0</span></h3>
<div id="<%= dom_id(post, :presence_list) %>">
<!-- Users will be appended here -->
</div>
</div>
<!-- same contents -->
</turbo-cable-presence-source>
If there is an element with the data-presence-counter
attribute, we update its text content with the current number of present users.
Something else we noted is that, in most situations, you don’t want to show the current active user on the presence list—instead, users are interested in who else is online. To make that happen, we’ve introduced the ignore-self
configuration parameter in the form of a data attribute:
<turbo-cable-presence-source
signed-stream-name="<%= signed_stream_name([post, :presence]) %>"
presence-id="<%= dom_id(user, :presence) %>"
ignore-self
>
<!-- same contents -->
</turbo-cable-presence-source>
As you can see, we can go pretty far by extending our HTML interface (i.e., adding new attributes). One thing that we definitely plan to implement is custom leave templates (so you will be able not only to remove DOM nodes, but also to do some other cleanup actions).
However, the more sophisticated the HTML interface becomes, the harder it is to justify the choice of going all-in with HTML. Luckily, you can always rely on the underlying AnyCable Presence API for Action Cable for building whatever you need.
A full-featured AnyCable/Rails/Hotwire example can be found in our demo: anycable/anycasts_demo#17.
Next, let’s rewind a bit and talk about how to get started with AnyCable Presence if you don’t have AnyCable in your stack yet.
Getting started with AnyCable for Hotwire
First, let’s recall that AnyCable is an open-source, (MIT-licensed) software. You can grab its source code from GitHub, Docker images from DockerHub, use all the SDKs and tools we provide for free. You can also quickly spin up a cable (that’s how we call AnyCable servers) using our managed offering, plus.anycable.io, again, for free. Finally, we offer a commercial version of the server called AnyCable Pro which gives you even more power.
So, the first ingredient is the AnyCable server.
From there, you also need to substitute the default Hotwire Action Cable client with the AnyCable one. AnyCable JavaScript client comes with many enhancements compared to the official Rails client (that hasn’t been updated in a while). These enhancements include: better subscription race conditions handling (ever seen continuous subscribe
messages coming from an Action Cable client?), flexible modular architecture, TypeScript support, and more.
And, of course, the AnyCable client knows how to speak AnyCable dialect of Action Cable protocol; and Presence is a part of it.
Finally, the <turbo-cable-presence-source>
element is also a part of the AnyCable Turbo Stream package (@anycable/turbo-stream
). So, all you need is to install it and configure your Turbo application:
import "@hotwired/turbo"
import { start } from "@anycable/turbo-stream"
import { createCable } from "@anycable/web"
const cable = createCable({ protocol: 'actioncable-v1-ext-json' })
start(cable, { presence: true })
One benefit of this setup is that it’s truly language-agnostic and framework-agnostic. So, combine Hotwire with AnyCable for the win, wherever you go!