Skip to main content

Realtime (Ably)

In one line: Ably holds an open WebSocket in the browser and pushes each change to everyone who should see it in ~50ms — booking status, schedule, and leave. Publishing is server-side only; clients get subscribe-only Token Auth scoped to their role. It degrades gracefully when ABLY_PRIVATE_KEY is absent.
When a receptionist approves a booking, the customer sitting on their bookings page should see the badge flip from “Pending” to “Confirmed” on its own — no refresh, no reload. Royal Glow does this with Ably, which holds an open WebSocket connection in the browser and pushes each change to everyone who should see it in roughly 50ms.
Ably is a guarded extension point. The server helper reads ABLY_PRIVATE_KEY directly behind a truthy guard. With the key absent it returns null, POST /api/ably/token answers 503, and the client falls back to its normal data fetch. The app builds and runs with no realtime configured — live updates simply switch on once the key is present.

What it is

A thin live layer over the normal app. The page still loads its data the usual way; Ably only delivers the changes that happen while the page is open. Two audiences benefit:
  • Customers see their booking cards update in place — status, date, and the assigned stylist — without touching anything.
  • Staff and admins see new bookings drop into the pending queue, the schedule grid fill and free up, and leave requests appear as they are made.
Ably’s free tier covers 6 million messages per month, comfortably more than a single-branch salon generates.

How it works

1

Page mounts

A useEffect opens an Ably connection using an authCallback that fetches a token from our own API.
2

Token is issued

POST /api/ably/token returns a short-lived, scoped Ably JWT for the signed-in user (see Token Auth).
3

Subscribe

The page subscribes to exactly the channels it is allowed to read and updates React state when a message arrives.
4

Server publishes

After a state-changing API route commits its database write, it publishes to every relevant channel using ABLY_PRIVATE_KEY.
5

Page unmounts

The connection is cleaned up. Ably handles reconnect on network loss and calls authCallback again when the token expires.
All publishing is server-side only. Browsers never hold a key with publish capability — they receive a subscribe-only token. This prevents anyone from spoofing a status change or publishing to a channel from the client.

Channels

Channels follow a predictable naming convention so a token can be scoped with wildcards.
The customer always subscribes to customer:{userId}:bookings on the bookings list, and to booking:{bookingId} on a booking detail. Admin dashboards subscribe to admin:bookings:{branchId}; the schedule page subscribes to admin:schedule:{selectedDate}.

Example events

A representative slice of the events flowing over these channels:

Token Auth

Every client uses Token Auth — never a raw key. The server mints a short-lived Ably JWT per authenticated user, scoped to exactly the channels that user’s role may read. POST /api/ably/token is a session-protected route; the clientId on the token is the caller’s user id.
A customer is granted subscribe on their own namespace only, plus the specific booking channels they own. They never receive a wildcard over all booking:* channels, so they cannot watch another customer’s booking.
Realtime degrades gracefully. When ABLY_PRIVATE_KEY is not configured (or the optional ably module is unavailable), POST /api/ably/token responds with 503 SERVICE_UNAVAILABLE and the client keeps working off its initial data fetch. No screen breaks just because realtime is off.

Publishing flow

State-changing API routes publish to every relevant channel after the database write commits, so a subscriber never sees a change that did not actually persist.
A customer cancellation fans out the same way — releasing the slot on admin:schedule:{date}, removing the appointment on the staff channel, and moving the card on admin:bookings:{branchId}. Because each publish is fire-and-forget after the commit, a realtime hiccup never blocks or fails the underlying booking action.
Last modified on June 29, 2026