Ably is a managed real-time messaging platform — new Ably.Realtime({ key }) creates a client with automatic reconnection. client.channels.get("channel-name") gets a channel reference. channel.publish("event", data) sends a message; channel.subscribe("event", callback) receives it. Presence tracks who is connected: channel.presence.enter({ cursor: { x, y } }), channel.presence.subscribe("enter", cb), and channel.presence.get(). Channel history retrieves past messages with channel.history({ limit: 50 }). Token authentication uses a server-side endpoint that returns a signed TokenRequest — never expose your Ably API key in the browser. The ably/react package provides useChannel and usePresence hooks. Connection state is managed through client.connection.on("connected", cb). Claude Code generates Ably client setup, channel pub/sub, presence tracking, server-side token auth, React hooks, and patterns for live order tracking and multiplayer cursor sharing.
CLAUDE.md for Ably
## Ably Stack
- Version: ably >= 2.3
- Client: new Ably.Realtime({ authUrl: "/api/ably-token" }) — never use key in browser
- Channel: const ch = client.channels.get("orders:${orderId}") — namespaced channels
- Publish: ch.publish("status-update", { status, updatedAt }) — server or admin only
- Subscribe: ch.subscribe("status-update", msg => setStatus(msg.data.status))
- Presence: ch.presence.enter({ userId, name }) / ch.presence.subscribe("enter", cb)
- React: useChannel("orders:123", "update", msg => ...) — from ably/react
- Server: const rest = new Ably.Rest({ key: ABLY_API_KEY }) — publish from API routes
- Auth: POST /api/ably-token returns TokenRequest from rest.auth.createTokenRequest()
Server-Side Token Auth
// app/api/ably-token/route.ts — never expose Ably key to browser
import Ably from "ably"
import { auth } from "@/lib/auth"
const ably = new Ably.Rest({ key: process.env.ABLY_API_KEY! })
export async function GET() {
const session = await auth()
if (!session?.user?.id) {
return Response.json({ error: "Unauthorized" }, { status: 401 })
}
// Request a token scoped to this user
const tokenRequest = await ably.auth.createTokenRequest({
clientId: session.user.id,
capability: {
// User can only subscribe to their own orders and global announcements
[`orders:${session.user.id}:*`]: ["subscribe", "presence"],
"announcements": ["subscribe"],
// Publishers (admins) get additional publish rights — granted separately
},
ttl: 3_600_000, // 1 hour
})
return Response.json(tokenRequest)
}
React Hooks Integration
// components/OrderTracker.tsx — real-time order status with Ably React hooks
"use client"
import { useChannel, usePresence } from "ably/react"
import { useState } from "react"
interface OrderStatus {
status: "pending" | "processing" | "shipped" | "delivered"
message?: string
updatedAt: string
}
export function OrderTracker({ orderId }: { orderId: string }) {
const [status, setStatus] = useState<OrderStatus | null>(null)
const [isSupport, setIsSupport] = useState(false)
// Subscribe to order status updates
const { channel } = useChannel(
`orders:${orderId}`,
"status-update",
msg => {
setStatus(msg.data as OrderStatus)
}
)
// Show who from support is viewing this order (presence)
const { presenceData } = usePresence(
`orders:${orderId}`,
{ role: "customer", orderId }
)
const supportViewers = presenceData.filter(p => p.data?.role === "support")
const statusColors = {
pending: "bg-yellow-100 text-yellow-800",
processing: "bg-blue-100 text-blue-800",
shipped: "bg-purple-100 text-purple-800",
delivered: "bg-green-100 text-green-800",
}
return (
<div className="border rounded-lg p-4 space-y-3">
<div className="flex items-center justify-between">
<h3 className="font-semibold">Order #{orderId.slice(-8)}</h3>
{supportViewers.length > 0 && (
<span className="text-xs text-muted-foreground flex items-center gap-1">
<span className="h-1.5 w-1.5 rounded-full bg-green-500 animate-pulse" />
Support is watching
</span>
)}
</div>
{status ? (
<div className="space-y-1">
<span className={`inline-block px-2 py-0.5 rounded text-xs font-medium ${statusColors[status.status]}`}>
{status.status}
</span>
{status.message && (
<p className="text-sm text-muted-foreground">{status.message}</p>
)}
<p className="text-xs text-muted-foreground">
Updated {new Date(status.updatedAt).toLocaleTimeString()}
</p>
</div>
) : (
<p className="text-sm text-muted-foreground">Connecting to live updates...</p>
)}
</div>
)
}
Ably Provider Setup
// app/layout.tsx — Ably React provider
import * as Ably from "ably"
import { AblyProvider, ChannelProvider } from "ably/react"
// Client component wrapper
"use client"
export function AblyClientProvider({ children }: { children: React.ReactNode }) {
const client = new Ably.Realtime({ authUrl: "/api/ably-token" })
return <AblyProvider client={client}>{children}</AblyProvider>
}
Server-Side Publishing
// lib/ably-server.ts — publish from API routes
import Ably from "ably"
const rest = new Ably.Rest({ key: process.env.ABLY_API_KEY! })
interface OrderStatusUpdate {
status: string
message?: string
updatedAt: string
}
// Publish order status update to all subscribers
export async function publishOrderUpdate(
orderId: string,
update: OrderStatusUpdate
) {
const channel = rest.channels.get(`orders:${orderId}`)
await channel.publish("status-update", update)
}
// Publish to multiple orders (batch)
export async function publishBulkUpdate(
orderIds: string[],
update: Omit<OrderStatusUpdate, "updatedAt">
) {
const now = new Date().toISOString()
await Promise.all(
orderIds.map(id =>
publishOrderUpdate(id, { ...update, updatedAt: now })
)
)
}
// Get channel history for late subscribers
export async function getOrderHistory(orderId: string, limit = 20) {
const channel = rest.channels.get(`orders:${orderId}`)
const history = await channel.history({ limit })
return history.items
}
Multiplayer Cursors
// components/CollaborativeCursors.tsx — shared cursor positions
"use client"
import { usePresence } from "ably/react"
import { useEffect, useRef } from "react"
interface CursorPosition {
x: number
y: number
name: string
color: string
}
const COLORS = ["#3b82f6", "#ef4444", "#10b981", "#f59e0b", "#8b5cf6"]
export function CollaborativeCursors({
roomId,
userName,
}: {
roomId: string
userName: string
}) {
const color = COLORS[userName.length % COLORS.length]!
const containerRef = useRef<HTMLDivElement>(null)
const { updateStatus, presenceData } = usePresence(
`cursors:${roomId}`,
{ x: 0, y: 0, name: userName, color } as CursorPosition
)
useEffect(() => {
const container = containerRef.current
if (!container) return
function handleMouseMove(e: MouseEvent) {
const rect = container!.getBoundingClientRect()
updateStatus({
x: ((e.clientX - rect.left) / rect.width) * 100, // % of container
y: ((e.clientY - rect.top) / rect.height) * 100,
name: userName,
color,
})
}
container.addEventListener("mousemove", handleMouseMove)
return () => container.removeEventListener("mousemove", handleMouseMove)
}, [updateStatus, userName, color])
const otherCursors = presenceData.filter(p => p.clientId !== undefined)
return (
<div ref={containerRef} className="relative w-full h-full">
{otherCursors.map(presence => {
const pos = presence.data as CursorPosition
return (
<div
key={presence.clientId}
className="absolute pointer-events-none transition-all duration-75"
style={{ left: `${pos.x}%`, top: `${pos.y}%` }}
>
{/* Cursor SVG */}
<svg width="20" height="20" viewBox="0 0 20 20" fill={pos.color}>
<path d="M0 0L12 8L8 12L0 0Z" />
</svg>
<span
className="text-xs text-white px-1.5 py-0.5 rounded whitespace-nowrap"
style={{ backgroundColor: pos.color }}
>
{pos.name}
</span>
</div>
)
})}
</div>
)
}
Connection State Management
// lib/ably-connection.ts — connection lifecycle + reconnection
import Ably from "ably"
export function createAblyClient() {
const client = new Ably.Realtime({
authUrl: "/api/ably-token",
// Retry configuration
disconnectedRetryTimeout: 5_000,
suspendedRetryTimeout: 30_000,
})
client.connection.on("connected", () => {
console.log("[Ably] Connected")
})
client.connection.on("disconnected", () => {
console.log("[Ably] Disconnected — retrying...")
})
client.connection.on("failed", () => {
console.error("[Ably] Connection failed — check network and token")
})
return client
}
For the Pusher alternative when a simpler WebSocket-backed pub/sub with a large ecosystem of framework adapters (Laravel, Django, Rails) is sufficient — Pusher Channels has a similar API but with a lower ceiling on presence channel size and no built-in message history, see the real-time comparison guide. For the Liveblocks alternative when the use case is specifically collaborative document editing with operational transforms, undo/redo history, presence cursors, and room-based access control — Liveblocks provides first-class data structures (LiveObject, LiveList) purpose-built for CRDT-based collaboration rather than general pub/sub, see the Liveblocks guide. The Claude Skills 360 bundle includes Ably skill sets covering channels, presence, token auth, and React hooks. Start with the free tier to try real-time messaging generation.