This guide will get you started with Ably Pub/Sub in a new Next.js application.
You'll establish a realtime connection to Ably and learn to publish and subscribe to messages. You'll also implement presence to track other online clients, and learn how to retrieve message history.
Prerequisites
- Sign up for an Ably account.
- Create a new app, and create your first API key in the API Keys tab of the dashboard.
- Your API key will need the
publish,subscribe,presenceandhistorycapabilities.
Create a Next.js project
Create a new Next.js project using the official scaffolding tool. Select App Router and TypeScript when prompted:
npx create-next-app@latest ably-pubsub-nextjs
cd ably-pubsub-nextjsUpdate globals.css
Replace the contents of src/app/globals.css with the following to reset browser defaults and ensure consistent font sizing across all elements including inputs and buttons:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/* src/app/globals.css */
html {
height: 100%;
}
html,
body {
max-width: 100vw;
overflow-x: hidden;
}
body {
min-height: 100%;
display: flex;
flex-direction: column;
color: #171717;
background: #ffffff;
font-family: Arial, Helvetica, sans-serif;
font-size: 15px;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
*,
input,
button {
box-sizing: border-box;
padding: 0;
margin: 0;
font-size: inherit;
font-family: inherit;
}Install Ably Pub/Sub JavaScript SDK
Install the Ably Pub/Sub JavaScript SDK:
npm install ably(Optional) Install Ably CLI
Use the Ably CLI as an additional client to quickly test Pub/Sub features. It can simulate other clients by publishing messages, subscribing to channels, and managing presence states.
- Install the Ably CLI:
npm install -g @ably/cli- Run the following to log in to your Ably account and set the default app and API key:
ably loginStep 1: Connect to Ably
Clients establish a connection with Ably when they instantiate an SDK instance. This enables them to send and receive messages in realtime across channels.
Open up the dev console of your first app before you start so that you can see what happens.
Set up AblyProvider
The Ably Pub/Sub SDK provides React hooks and context providers that make it easy to use Pub/Sub features in your components.
Because the Ably Pub/Sub client uses browser APIs such as WebSocket, it cannot run during server-side rendering. Create a new file src/app/AblyProvider.tsx that initializes the client inside a useEffect and wraps children in the AblyProvider:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// src/app/AblyProvider.tsx
'use client';
import * as Ably from 'ably';
import { AblyProvider as AblyReactProvider } from 'ably/react';
import { ReactNode, useEffect, useState } from 'react';
export function AblyProvider({ children }: { children: ReactNode }) {
const [client, setClient] = useState<Ably.Realtime | null>(null);
useEffect(() => {
const ably = new Ably.Realtime({
key: 'demokey:*****',
clientId: 'my-first-client',
});
setClient(ably);
return () => {
ably.close();
};
}, []);
if (!client) return null;
return <AblyReactProvider client={client}>{children}</AblyReactProvider>;
}Add the AblyProvider to your root layout in src/app/layout.tsx:
1// src/app/layout.tsx2import type { ReactNode } from 'react';3import { AblyProvider } from './AblyProvider';4 5export default function RootLayout({ children }: { children: ReactNode }) {6 return (7 <html lang='en'>8 <body>9 <AblyProvider>{children}</AblyProvider>10 </body>11 </html>12 );13}This establishes a connection to Ably as soon as your application mounts in the browser. While using an API key is fine for this guide, you should use token authentication in production. A clientId identifies the client, which is required for features such as presence.
Display the connection state
To display the connection state in your UI, create a client component at src/app/ConnectionState.tsx:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// src/app/ConnectionState.tsx
'use client';
import { useAbly, useConnectionStateListener } from 'ably/react';
import { useState } from 'react';
export function ConnectionState() {
const ably = useAbly();
const [connectionState, setConnectionState] = useState(ably.connection.state);
useConnectionStateListener((stateChange) => {
setConnectionState(stateChange.current);
});
return (
<p style={{ color: '#555', padding: '8px 10px', background: '#f0f0f0', borderRadius: '5px' }}>
Connection: <strong>{connectionState}</strong>
</p>
);
}Update src/app/page.tsx to render the component:
1// src/app/page.tsx2import { ConnectionState } from './ConnectionState';3 4export default function Home() {5 return (6 <main style={{ fontFamily: 'Arial, sans-serif', padding: '20px', maxWidth: '900px', margin: '0 auto' }}>7 <h1 style={{ marginBottom: '20px', fontSize: '36px', textAlign: 'center' }}>Ably Pub/Sub - Next.js</h1>8 <ConnectionState />9 </main>10 );11}Start the development server:
npm run devOpen http://localhost:3000 and you should see Connection: connected. You can also inspect the connection event in the dev console of your app.
Step 2: Subscribe to a channel and publish a message
To publish and subscribe to messages on a channel use the ChannelProvider component from the Ably Pub/Sub SDK, which scopes child components to a specific channel.
ChannelProvider
The ChannelProvider must be nested inside the AblyProvider. Update src/app/page.tsx to include the ChannelProvider:
1// src/app/page.tsx2'use client';3 4import { ChannelProvider } from 'ably/react';5import { ConnectionState } from './ConnectionState';6 7export default function Home() {8 return (9 <main style={{ fontFamily: 'Arial, sans-serif', padding: '20px', maxWidth: '900px', margin: '0 auto' }}>10 <h1 style={{ marginBottom: '20px', fontSize: '36px', textAlign: 'center' }}>Ably Pub/Sub - Next.js</h1>11 <ConnectionState />12 <ChannelProvider channelName='my-first-channel'>13 {/* Channel-scoped components go here */}14 </ChannelProvider>15 </main>16 );17}Subscribe to a channel
Use the useChannel() hook to subscribe to messages on a channel. Create a new file src/app/Messages.tsx:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// src/app/Messages.tsx
'use client';
import type { Message } from 'ably';
import { useChannel } from 'ably/react';
import { useState } from 'react';
export function Messages() {
const [messages, setMessages] = useState<Message[]>([]);
useChannel('my-first-channel', (message) => {
setMessages((prev) => [...prev, message]);
});
return (
<div style={{ border: '1px solid #ddd', borderRadius: '4px', padding: '12px', width: '400px', height: '250px', overflowY: 'auto', background: '#fff' }}>
{messages.map((msg) => (
<p key={msg.id} style={{ margin: '4px 0', borderLeft: '3px solid #007bff', paddingLeft: '8px', color: '#171717' }}>
{String(msg.data)}
</p>
))}
</div>
);
}Add Messages to page.tsx inside the ChannelProvider:
1// src/app/page.tsx2'use client';3 4import { ChannelProvider } from 'ably/react';5import { ConnectionState } from './ConnectionState';6import { Messages } from './Messages';7 8export default function Home() {9 return (10 <main style={{ fontFamily: 'Arial, sans-serif', padding: '20px', maxWidth: '900px', margin: '0 auto' }}>11 <h1 style={{ marginBottom: '20px', fontSize: '36px', textAlign: 'center' }}>Ably Pub/Sub - Next.js</h1>12 <ConnectionState />13 <ChannelProvider channelName='my-first-channel'>14 <Messages />15 </ChannelProvider>16 </main>17 );18}Test it by publishing a message from the CLI:
ably channels publish my-first-channel 'Hello from CLI!'Publish a message
The useChannel() hook also returns a publish method. Update src/app/Messages.tsx to add a message input:
1// src/app/Messages.tsx2'use client';3 4import type { Message } from 'ably';5import { useChannel } from 'ably/react';6import { useState } from 'react';7 8export function Messages() {9 const [messages, setMessages] = useState<Message[]>([]);10 const [inputValue, setInputValue] = useState('');11 12 const { publish } = useChannel('my-first-channel', (message) => {13 setMessages((prev) => [...prev, message]);14 });15 16 const handlePublish = () => {17 if (!inputValue.trim()) return;18 publish('my-first-messages', inputValue.trim()).catch(console.error);19 setInputValue('');20 };21 22 return (23 <div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>24 <div style={{ border: '1px solid #ddd', borderRadius: '4px', padding: '12px', width: '400px', height: '250px', overflowY: 'auto', background: '#fff' }}>25 {messages.map((msg) => (26 <p key={msg.id} style={{ margin: '4px 0', borderLeft: '3px solid #007bff', paddingLeft: '8px', color: '#171717' }}>27 <span style={{ color: '#777', marginRight: '6px' }}>{msg.clientId}:</span>28 {String(msg.data)}29 </p>30 ))}31 </div>32 <div style={{ display: 'flex', gap: '8px' }}>33 <input34 type='text'35 value={inputValue}36 placeholder='Type a message...'37 onChange={(e) => setInputValue(e.target.value)}38 onKeyDown={(e) => e.key === 'Enter' && handlePublish()}39 style={{ flex: 1, padding: '10px 12px', border: '1px solid #ccc', borderRadius: '4px', outline: 'none' }}40 />41 <button42 onClick={handlePublish}43 style={{ padding: '10px 20px', background: '#007bff', color: '#fff', border: 'none', borderRadius: '4px', cursor: 'pointer' }}44 >45 Publish46 </button>47 </div>48 </div>49 );50}Type a message and click Publish to see it appear in your UI. Open another browser window to see messages arriving in realtime.
Step 3: Join the presence set
Presence enables clients to be aware of one another on the same channel. You can show who is online, provide status updates, and notify the channel when someone goes offline.
Use the usePresence() and usePresenceListener() hooks from the Ably Pub/Sub SDK. Create a new file src/app/PresenceStatus.tsx:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// src/app/PresenceStatus.tsx
'use client';
import { usePresence, usePresenceListener } from 'ably/react';
export function PresenceStatus() {
usePresence('my-first-channel', { status: "I'm here!" });
const { presenceData } = usePresenceListener('my-first-channel');
return (
<div style={{ padding: '8px 10px', background: '#f0f0f0', borderRadius: '5px' }}>
<h3 style={{ marginTop: 0, marginBottom: '12px', fontSize: '18px' }}>
Present ({presenceData.length})
</h3>
<ul style={{ listStyle: 'none', padding: 0, margin: 0 }}>
{presenceData.map((member, idx) => (
<li key={idx} style={{ padding: '4px 0', color: '#333' }}>
<span style={{ display: 'inline-block', width: 8, height: 8, borderRadius: '50%', background: '#28a745', marginRight: 6 }} />
{member.clientId}
{member.data?.status ? <span style={{ color: '#777' }}> - {member.data.status}</span> : null}
</li>
))}
</ul>
</div>
);
}Update src/app/page.tsx to include PresenceStatus and ConnectionState inside the ChannelProvider, alongside Messages:
1// src/app/page.tsx2'use client';3 4import { ChannelProvider } from 'ably/react';5import { ConnectionState } from './ConnectionState';6import { Messages } from './Messages';7import { PresenceStatus } from './PresenceStatus';8 9export default function Home() {10 return (11 <main style={{ fontFamily: 'Arial, sans-serif', padding: '20px', maxWidth: '900px', margin: '0 auto' }}>12 <h1 style={{ marginBottom: '20px', fontSize: '36px', textAlign: 'center' }}>Ably Pub/Sub - Next.js</h1>13 <ChannelProvider channelName='my-first-channel'>14 <div style={{ display: 'flex', gap: '10px' }}>15 <div style={{ flex: '0 0 220px', display: 'flex', flexDirection: 'column', gap: '10px' }}>16 <PresenceStatus />17 <ConnectionState />18 </div>19 <div style={{ flex: 1 }}>20 <Messages />21 </div>22 </div>23 </ChannelProvider>24 </main>25 );26}Your client ID will appear in the presence list. Join presence via the CLI to see another client joining:
ably channels presence enter my-first-channel --data '{"status":"From CLI"}'Step 4: Retrieve message history
Ably stores messages for 2 minutes by default. You can extend the storage period if required.
The useChannel() hook returns a channel instance. Use its history() method to load previously published messages on mount. Update your Messages component in src/app/Messages.tsx to load history with a useEffect:
1// src/app/Messages.tsx2'use client';3 4import type { Message } from 'ably';5import { useChannel } from 'ably/react';6import { useEffect, useState } from 'react';7 8export function Messages() {9 const [messages, setMessages] = useState<Message[]>([]);10 const [inputValue, setInputValue] = useState('');11 12 const { publish, channel } = useChannel('my-first-channel', (message) => {13 setMessages((prev) => [...prev, message]);14 });15 16 useEffect(() => {17 async function loadHistory() {18 const history = await channel.history({ limit: 5 });19 setMessages((prev) => [...history.items.reverse(), ...prev]);20 }21 loadHistory().catch(console.error);22 }, [channel]);23 24 const handlePublish = () => {25 if (!inputValue.trim()) return;26 publish('my-first-messages', inputValue.trim()).catch(console.error);27 setInputValue('');28 };29 30 return (31 <div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>32 <div style={{ border: '1px solid #ddd', borderRadius: '4px', padding: '12px', width: '400px', height: '250px', overflowY: 'auto', background: '#fff' }}>33 {messages.map((msg) => (34 <p key={msg.id} style={{ margin: '4px 0', borderLeft: '3px solid #007bff', paddingLeft: '8px', color: '#171717' }}>35 <span style={{ color: '#777', marginRight: '6px' }}>{msg.clientId}:</span>36 {String(msg.data)}37 </p>38 ))}39 </div>40 <div style={{ display: 'flex', gap: '8px' }}>41 <input42 type='text'43 value={inputValue}44 placeholder='Type a message...'45 onChange={(e) => setInputValue(e.target.value)}46 onKeyDown={(e) => e.key === 'Enter' && handlePublish()}47 style={{ flex: 1, padding: '10px 12px', border: '1px solid #ccc', borderRadius: '4px', outline: 'none' }}48 />49 <button50 onClick={handlePublish}51 style={{ padding: '10px 20px', background: '#007bff', color: '#fff', border: 'none', borderRadius: '4px', cursor: 'pointer' }}52 >53 Publish54 </button>55 </div>56 </div>57 );58}Publish a few messages first if needed:
ably channels publish --count 5 my-first-channel "Message number {{.Count}}"Reload the page. The last 5 messages will appear immediately, loaded from history before any new realtime messages arrive.
Your completed application should look like this:
Next steps
Continue to explore the documentation with Next.js as the selected language:
- Understand token authentication before going to production.
- Understand how to effectively manage connections.
- Explore more advanced Pub/Sub concepts.
You can also explore the Ably CLI further, visit the Pub/Sub API references, or browse the Ably Next.js Fundamentals Kit for more complete examples.