Royal Glow internal docs · now fully interactive — Steps, API tables, file trees & live status
Royal Glow Docs

Realtime (Ably)

Live booking status, schedule, and leave updates pushed to customers and staff over Ably — scoped per role with server-only publishing and Token Auth.

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

Page mounts

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

Token is issued

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

Subscribe

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

Server publishes

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

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.

Shared channels:   {namespace}:{topic}
Scoped channels:   {namespace}:{identifier}:{sub-topic}
ChannelAudienceCarries
customer:{userId}:bookingsA single customer's browserAll of that customer's booking card updates (created, status changed, rescheduled, cancelled, staff assigned)
booking:{bookingId}Customer + admins + assigned staffOne booking's detail: status changes, staff notes, services added/removed
admin:bookings:{branchId}Developer, Owner, Manager, ReceptionistNew bookings, status changes, walk-ins, cancellations, no-shows for a branch's dashboard
admin:schedule:{date}Schedule viewers (date-scoped, YYYY-MM-DD)Slots booked/released, staff marked off, leave approved for that day
staff:{staffId}:scheduleOne staff memberTheir assignments, unassignments, and leave request outcomes

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:

ChannelEventUI effect
customer:{userId}:bookingsbooking.status_changedStatus badge animates to the new state in place
booking:{bookingId}note.addedA staff note appears live on the admin booking detail
admin:bookings:{branchId}booking.newA new card slides into the top of the pending queue
admin:schedule:{date}slot.bookedThe time slot turns blocked in the grid
staff:{staffId}:schedulebooking.assignedA new appointment appears on the staff member's day

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.

subscribe: ["customer:usr_abc123:*", "booking:bkg_xyz789"]
publish:   []   ← never

Roles receptionist and above additionally receive subscribe on the admin:* namespace, covering the branch dashboard, schedule, and leave channels.

subscribe: ["customer:usr_abc123:*", "admin:*", "booking:*"]
publish:   []   ← never

A staff member is scoped to their own schedule channel and only the booking channels they are assigned to view.

subscribe: ["staff:stf_def456:schedule", "booking:bkg_xyz789"]
publish:   []   ← never

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.

PATCH admin.theroyalglow.in/api/bookings/:id   (action: approve)
  → DB: booking.status = 'confirmed'
  → Ably publish:
      customer:{userId}:bookings   →  booking.status_changed  { toStatus: 'confirmed' }
      booking:{bookingId}          →  status.changed
      admin:bookings:{branchId}    →  booking.status_changed
      staff:{staffId}:schedule     →  booking.assigned
      admin:schedule:{date}        →  slot.booked

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.

OpenReport an issue

Was this page helpful?

On this page