Claude Code for Ably: Real-Time Messaging and Pub/Sub — Claude Skills 360 Blog
Blog / Backend / Claude Code for Ably: Real-Time Messaging and Pub/Sub
Backend

Claude Code for Ably: Real-Time Messaging and Pub/Sub

Published: March 27, 2027
Read time: 7 min read
By: Claude Skills 360

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.

Keep Reading

Backend

Claude Code for Bun: Fast JavaScript Runtime and Toolkit

Build with Bun and Claude Code — Bun.serve for HTTP servers, Bun.file for fast file I/O, Bun.$ for shell commands, Bun.sql for SQLite and PostgreSQL, Bun.build for bundling, bun:test for testing, Bun.hash for hashing, bun.lock for deterministic installs, bun run for package.json scripts, hot reloading with --hot, bun init for project scaffolding, and compatibility with Node.js modules.

6 min read Jun 13, 2027
Backend

Claude Code for Express.js Advanced: Patterns for Production APIs

Advanced Express.js patterns with Claude Code — typed request handlers with RequestHandler generics, async error handling middleware, Zod validation middleware factory, rate limiting with express-rate-limit and Redis store, helmet security middleware, compression, dependency injection with tsyringe, file upload with multer and S3, pagination utilities, JWT middleware, and structured logging with pino.

6 min read Jun 8, 2027
Backend

Claude Code for KeystoneJS: Node.js CMS and App Framework

Build full-stack apps with KeystoneJS and Claude Code — config with lists, fields.text and fields.relationship for schema definition, access control with isAuthenticated and isAdmin functions, hooks with beforeOperation and afterOperation, GraphQL API auto-generation from schema, AdminUI for content management, session with statelessSessions, Prisma adapter for database, file storage with images and files fields, and custom REST endpoints.

6 min read Jun 7, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free