Lesson 5 — Presence
What presence means
Presence shows which board members are currently viewing the board. Rather than two separate lists — one for all members, one for online members — we use a single row of avatars where online members get a green dot. This mirrors how Slack, Notion, and Linear handle it: one list, two visual states.
The implementation uses ActionCable with a dedicated presence channel. When a user opens the board, their browser subscribes and announces their arrival. When they leave, the browser unsubscribes and announces their departure. The channel broadcasts the updated online list to all subscribers.
Seed data
Before testing presence, update the seed names to give meaningful two-letter initials:
|
|
Now avatars show AS, BW, and CH rather than A, B, C.
Reseed:
|
|
ApplicationCable::Connection
The presence channel needs current_user. Wire it up in the ActionCable
connection using the same signed cookie that Authentication uses:
|
|
|
|
PresenceChannel
|
|
Rails.cache stores the current online list. In development the
default memory cache works fine for a single server process. In
production with multiple processes, configure Redis or Solid Cache.
ActionCable consumer
Create the consumer file if it doesn’t exist:
|
|
Pin it in importmap.rb:
|
|
Components::MemberAvatars
One component handles both jobs — rendering all members and highlighting the ones currently online with a green dot:
|
|
Each member div has data-presence-target="member" and
data-presence-user-id so the Stimulus controller can identify and
update it. No green dot is rendered server-side — the controller adds
and removes dots based on the broadcast.
presence_controller.js
|
|
updatePresence receives the broadcast payload — an array of online
member objects. It compares each member avatar’s presenceUserId
against the online list and adds or removes the green dot accordingly.
The dot uses the class presence-dot as a stable selector so
el.querySelector(".presence-dot") reliably finds it for removal.
Tailwind utility classes alone aren’t reliable selectors since they
can combine with other classes.
Adding MemberAvatars to the board view
Replace any previous PresenceBar or separate avatar references in
Views::Boards::Show#render_header with the single unified component:
|
|
Testing presence
To test with multiple simultaneous users you need separate browser cookie stores — two windows or tabs of the same browser share cookies and will overwrite each other’s session.
Use any of these combinations:
- Chrome + Firefox — completely separate cookie stores
- Chrome + Chrome Incognito — incognito has its own isolated store
- Two different browser profiles — each maintains its own cookies
Sign in as Alice in one browser and Bob in another. Open the same board in both. Alice’s avatar should get a green dot in Bob’s browser, and Bob’s avatar should get a green dot in Alice’s browser. Close one browser — that user’s dot should disappear within a few seconds.
This is a development concern only. In production each user has their own device and browser — the issue doesn’t arise.
Module 11 summary
Access policy
| Action | Member | Admin |
|---|---|---|
| View board | ✅ | ✅ |
| Create card | ✅ | ✅ |
| Edit card | ✅ | ✅ |
| Delete card | ✅ | ✅ |
| Move card | ✅ | ✅ |
| Edit column | ❌ | ✅ |
| Delete column | ❌ | ✅ |
| Add column | ❌ | ✅ |
| Edit board | ❌ | ✅ |
| Delete board | ❌ | ✅ |
| Manage members | ❌ | ✅ |
Card deletion is open to all members — a known simplification. To
restrict it to the card creator or admins, add created_by_id to
cards and add a delete_card? method to BoardPolicy that checks
card.created_by == user || admin?.
What we built
BoardPolicy— plain Ruby object, no Pundit, one method per permissionBoardMembersController— add existing users by email, remove members, protect admin membership from removal- Policy-driven UI —
KanbanColumnandKanbanCardshow/hide controls based on role PresenceChannel— ActionCable channel tracking connected members per board viaRails.cachePresenceBarcomponent — avatars of connected memberspresence_controller.js— subscribes on connect, updates avatars from broadcast