Next.js quickstart
Add real-time MQTT to your Next.js app with the CloudSignal SDK and server-side token authentication.
This quickstart wires up the secure browser pattern in Next.js: a server route mints a short-lived token using your CloudSignal secret key, the browser exchanges that token for an MQTT connection, and a React component subscribes and publishes.
Why server-side tokens? Browser code is visible to users. Never put long-lived MQTT credentials in client-side code. Mint short-lived tokens on your server instead.
What you'll build
| Piece | Role |
|---|---|
| Server route | Mints MQTT credentials for the signed-in user |
| React hook | Manages the SDK client lifecycle |
| Live component | Subscribes and publishes from the browser |
Install dependencies
npm install @cloudsignal/mqtt-clientSet environment variables
Add to your .env.local:
# CloudSignal credentials (keep secret)
CLOUDSIGNAL_ORG_ID=org_your_short_id
CLOUDSIGNAL_SECRET_KEY=sk_your_secret_key
# Public config (safe for the client)
NEXT_PUBLIC_CLOUDSIGNAL_ORG_ID=org_your_short_idGet your sk_ key from Dashboard -> API Keys. Legacy cs_ keys still work.
Create the token route
Create app/api/mqtt-token/route.ts:
import { NextResponse } from 'next/server';
export async function POST(request: Request) {
// In production, verify user authentication here
const { userEmail } = await request.json();
const response = await fetch('https://api.cloudsignal.app/v2/tokens/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
organization_id: process.env.CLOUDSIGNAL_ORG_ID,
secret_key: process.env.CLOUDSIGNAL_SECRET_KEY,
user_email: userEmail,
}),
});
if (!response.ok) {
return NextResponse.json({ error: 'Failed to generate token' }, { status: 502 });
}
const data = await response.json();
return NextResponse.json({
accessToken: data.access_token,
tokenId: data.token_id,
expiresAt: data.expires_at,
refreshAt: data.refresh_recommended_at,
});
}Create the MQTT hook
Create hooks/use-mqtt.ts:
'use client';
import { useCallback, useEffect, useRef, useState } from 'react';
import CloudSignal from '@cloudsignal/mqtt-client';
interface UseMqttOptions {
userId: string;
userEmail: string;
onMessage?: (topic: string, message: unknown) => void;
}
export function useMqtt({ userId, userEmail, onMessage }: UseMqttOptions) {
const [client, setClient] = useState<CloudSignal | null>(null);
const [isConnected, setIsConnected] = useState(false);
const [error, setError] = useState<string | null>(null);
const onMessageRef = useRef(onMessage);
useEffect(() => {
onMessageRef.current = onMessage;
}, [onMessage]);
useEffect(() => {
let active = true;
let c: CloudSignal | null = null;
async function connect() {
try {
const res = await fetch('/api/mqtt-token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userEmail }),
});
if (!res.ok) throw new Error('Failed to get MQTT token');
const { accessToken } = await res.json();
c = new CloudSignal({
preset: 'desktop',
clientId: `web-${userId}-${Date.now()}`,
});
c.onMessage((topic, message) => {
onMessageRef.current?.(topic, message);
});
await c.connectWithToken({
organizationId: process.env.NEXT_PUBLIC_CLOUDSIGNAL_ORG_ID!,
externalToken: accessToken,
});
if (!active) {
c.destroy();
return;
}
setClient(c);
setIsConnected(true);
} catch (err) {
setError(err instanceof Error ? err.message : 'Connection failed');
}
}
connect();
return () => {
active = false;
c?.destroy();
};
}, [userId, userEmail]);
const subscribe = useCallback(async (topic: string) => {
await client?.subscribe(topic);
}, [client]);
const publish = useCallback((topic: string, message: unknown) => {
client?.transmit(topic, message);
}, [client]);
return { isConnected, error, subscribe, publish };
}Build a real-time component
Create components/live-chat.tsx:
'use client';
import { useEffect, useState } from 'react';
import { useMqtt } from '@/hooks/use-mqtt';
interface Message {
user: string;
text: string;
timestamp: number;
}
export function LiveChat({
userId,
userEmail,
roomId,
}: {
userId: string;
userEmail: string;
roomId: string;
}) {
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState('');
const { isConnected, error, subscribe, publish } = useMqtt({
userId,
userEmail,
onMessage: (topic, message) => {
if (topic === `chat/${roomId}/messages`) {
setMessages((prev) => [...prev, message as Message]);
}
},
});
useEffect(() => {
if (isConnected) {
subscribe(`chat/${roomId}/messages`);
}
}, [isConnected, roomId, subscribe]);
const sendMessage = () => {
if (!input.trim()) return;
publish(`chat/${roomId}/messages`, {
user: userId,
text: input,
timestamp: Date.now(),
});
setInput('');
};
return (
<div className="flex flex-col h-96 border rounded-lg">
<div className="p-2 border-b flex items-center gap-2">
<div className={`w-2 h-2 rounded-full ${isConnected ? 'bg-green-500' : 'bg-red-500'}`} />
<span className="text-sm">
{isConnected ? 'Connected' : error || 'Connecting...'}
</span>
</div>
<div className="flex-1 overflow-y-auto p-4 space-y-2">
{messages.map((msg, i) => (
<div
key={i}
className={`p-2 rounded ${msg.user === userId ? 'bg-blue-100 ml-auto' : 'bg-gray-100'}`}
>
<span className="font-bold text-sm">{msg.user}: </span>
<span>{msg.text}</span>
</div>
))}
</div>
<div className="p-2 border-t flex gap-2">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && sendMessage()}
placeholder="Type a message..."
className="flex-1 px-3 py-2 border rounded"
disabled={!isConnected}
/>
<button
onClick={sendMessage}
disabled={!isConnected}
className="px-4 py-2 bg-blue-500 text-white rounded disabled:opacity-50"
>
Send
</button>
</div>
</div>
);
}Use it in a page
Create app/chat/page.tsx:
import { LiveChat } from '@/components/live-chat';
export default function ChatPage() {
// In production, pull these from your auth layer
const userId = 'user-123';
const userEmail = 'user@example.com';
const roomId = 'general';
return (
<main className="container mx-auto p-8">
<h1 className="text-2xl font-bold mb-4">Live chat</h1>
<LiveChat userId={userId} userEmail={userEmail} roomId={roomId} />
</main>
);
}Token refresh
Tokens expire after their TTL. Call /v2/tokens/refresh at refresh_recommended_at with the current token_id and password to get new credentials, then reconnect:
// In your server-side API, expose /api/mqtt-token/refresh that forwards
// token_id + current_token_password to /v2/tokens/refresh and returns the new
// mqtt_credentials. Then, in use-mqtt.ts:
useEffect(() => {
if (!credentials) return;
const ms = new Date(credentials.refreshAt).getTime() - Date.now();
if (ms <= 0) return;
const timeout = setTimeout(async () => {
const res = await fetch('/api/mqtt-token/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tokenId: credentials.tokenId, password: credentials.password }),
});
if (!res.ok) return; // fall through and let the connection drop on expiry
const next = await res.json();
client?.destroy();
// reconnect with next.accessToken
}, ms);
return () => clearTimeout(timeout);
}, [credentials, client]);Production checklist
- Verify user authentication before issuing tokens
- Serve all traffic over HTTPS in production
- Set ACLs to restrict token users to the topics they need
- Handle reconnection gracefully
- Add error boundaries around MQTT components
Related
- Server-side tokens - Deep dive on token authentication
- Node.js quickstart - Backend MQTT patterns