Next.js Integration
Use CloudSignal MQTT client in Next.js applications with Supabase
Next.js Integration
Best practices for integrating CloudSignal into Next.js applications.
Installation
npm install @cloudsignal/mqtt-client
Client-Side Only
CloudSignal MQTT uses WebSockets, so it must run client-side only. Always use the "use client" directive.
Quick Start Hook
Create a reusable hook with React StrictMode support:
// hooks/useMQTT.ts
"use client";
import { useEffect, useRef, useState, useCallback } from 'react';
import CloudSignal from '@cloudsignal/mqtt-client';
interface Message {
topic: string;
payload: unknown;
receivedAt: number;
}
export function useMQTT(options = {}) {
const {
debug = false,
tokenServiceUrl = 'https://auth.cloudsignal.app',
preset = 'desktop'
} = options;
const [isConnected, setIsConnected] = useState(false);
const [error, setError] = useState<Error | null>(null);
const [messages, setMessages] = useState<Message[]>([]);
// Refs prevent StrictMode double-connection
const clientRef = useRef(null);
const connectingRef = useRef(false);
const mountedRef = useRef(true);
const connect = useCallback(async (config) => {
if (connectingRef.current || clientRef.current) return;
connectingRef.current = true;
setError(null);
try {
const client = new CloudSignal({ debug, preset, tokenServiceUrl });
client.onConnectionStatusChange = (connected) => {
if (mountedRef.current) setIsConnected(connected);
};
client.onAuthError = (err) => {
if (mountedRef.current) setError(err);
clientRef.current = null;
};
client.onMessage((topic, message) => {
const payload = (() => {
try { return JSON.parse(message); }
catch { return message; }
})();
setMessages(prev =>
[{ topic, payload, receivedAt: Date.now() }, ...prev].slice(0, 100)
);
});
await client.connectWithToken(config);
if (!mountedRef.current) {
client.destroy();
return;
}
clientRef.current = client;
} catch (err) {
if (mountedRef.current) setError(err);
} finally {
connectingRef.current = false;
}
}, [debug, preset, tokenServiceUrl]);
const disconnect = useCallback(() => {
clientRef.current?.destroy();
clientRef.current = null;
setIsConnected(false);
}, []);
useEffect(() => {
mountedRef.current = true;
return () => {
mountedRef.current = false;
clientRef.current?.destroy();
};
}, []);
return {
isConnected,
error,
messages,
connect,
disconnect,
subscribe: (topic, qos = 1) => clientRef.current?.subscribe(topic, qos),
publish: (topic, msg, opts) => clientRef.current?.transmit(topic, typeof msg === 'string' ? msg : JSON.stringify(msg), opts),
};
}
With Supabase Auth
Complete integration with automatic token refresh:
// hooks/useMQTTWithSupabase.ts
"use client";
import { useEffect, useRef, useState, useCallback } from 'react';
import CloudSignal from '@cloudsignal/mqtt-client';
import { createBrowserClient } from '@supabase/ssr';
const supabase = createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
export function useMQTTWithSupabase({ autoConnect = true, initialTopics = [] } = {}) {
const [isConnected, setIsConnected] = useState(false);
const [messages, setMessages] = useState([]);
const clientRef = useRef(null);
const connectingRef = useRef(false);
const mountedRef = useRef(true);
const connect = useCallback(async () => {
if (connectingRef.current || clientRef.current) return;
const { data: { session } } = await supabase.auth.getSession();
if (!session?.access_token) return;
connectingRef.current = true;
try {
const client = new CloudSignal({
tokenServiceUrl: 'https://auth.cloudsignal.app',
preset: 'desktop',
});
client.onConnectionStatusChange = (connected) => {
if (mountedRef.current) setIsConnected(connected);
};
client.onAuthError = () => {
clientRef.current = null; // Allow reconnect
};
client.onMessage((topic, message) => {
try {
setMessages(prev => [{ topic, payload: JSON.parse(message) }, ...prev].slice(0, 100));
} catch {
setMessages(prev => [{ topic, payload: message }, ...prev].slice(0, 100));
}
});
await client.connectWithToken({
host: 'wss://connect.cloudsignal.app:18885/',
organizationId: process.env.NEXT_PUBLIC_CLOUDSIGNAL_ORG_ID!,
externalToken: session.access_token,
});
if (!mountedRef.current) {
client.destroy();
return;
}
clientRef.current = client;
// Subscribe to initial topics
for (const topic of initialTopics) {
await client.subscribe(topic);
}
} finally {
connectingRef.current = false;
}
}, [initialTopics]);
const disconnect = useCallback(() => {
clientRef.current?.destroy();
clientRef.current = null;
setIsConnected(false);
}, []);
useEffect(() => {
mountedRef.current = true;
if (autoConnect) connect();
// Handle auth state changes (token refresh, sign out)
const { data: { subscription } } = supabase.auth.onAuthStateChange((event, session) => {
if (event === 'SIGNED_OUT') {
disconnect();
} else if (event === 'TOKEN_REFRESHED' && session) {
// Reconnect with new token
disconnect();
setTimeout(connect, 100);
} else if (event === 'SIGNED_IN' && !clientRef.current) {
connect();
}
});
return () => {
mountedRef.current = false;
subscription.unsubscribe();
disconnect();
};
}, [autoConnect, connect, disconnect]);
return {
isConnected,
messages,
subscribe: (topic) => clientRef.current?.subscribe(topic),
publish: (topic, msg) => clientRef.current?.transmit(topic, typeof msg === 'string' ? msg : JSON.stringify(msg)),
disconnect,
};
}
Context Provider
For app-wide access:
// contexts/MQTTContext.tsx
"use client";
import { createContext, useContext, ReactNode } from 'react';
import { useMQTTWithSupabase } from '@/hooks/useMQTTWithSupabase';
const MQTTContext = createContext(null);
export function MQTTProvider({ children }: { children: ReactNode }) {
const mqtt = useMQTTWithSupabase();
return (
<MQTTContext.Provider value={mqtt}>
{children}
</MQTTContext.Provider>
);
}
export function useMQTTContext() {
const context = useContext(MQTTContext);
if (!context) {
throw new Error('useMQTTContext must be used within MQTTProvider');
}
return context;
}
Root Layout
// app/layout.tsx
import { MQTTProvider } from '@/contexts/MQTTContext';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<MQTTProvider>{children}</MQTTProvider>
</body>
</html>
);
}
Real-Time Dashboard
// app/dashboard/page.tsx
"use client";
import { useEffect } from 'react';
import { useMQTTContext } from '@/contexts/MQTTContext';
export default function DashboardPage() {
const { isConnected, messages, subscribe } = useMQTTContext();
useEffect(() => {
if (isConnected) {
subscribe('metrics/cpu');
subscribe('metrics/memory');
subscribe('alerts/#');
}
}, [isConnected]);
const cpuData = messages.find(m => m.topic === 'metrics/cpu');
const memData = messages.find(m => m.topic === 'metrics/memory');
const alerts = messages.filter(m => m.topic.startsWith('alerts/'));
return (
<div className="grid grid-cols-2 gap-4 p-4">
<div className="card">
<h2>CPU Usage</h2>
<span className="text-4xl">{cpuData?.payload?.value ?? '--'}%</span>
</div>
<div className="card">
<h2>Memory</h2>
<span className="text-4xl">{memData?.payload?.value ?? '--'}%</span>
</div>
<div className="card col-span-2">
<h2>Recent Alerts ({alerts.length})</h2>
<ul>
{alerts.slice(0, 5).map((alert, i) => (
<li key={i} className={`alert-${alert.payload.level}`}>
{alert.payload.message}
</li>
))}
</ul>
</div>
</div>
);
}
With NextAuth.js
// hooks/useMQTTWithNextAuth.ts
"use client";
import { useSession } from 'next-auth/react';
import { useEffect, useRef, useState, useCallback } from 'react';
import CloudSignal from '@cloudsignal/mqtt-client';
export function useMQTTWithNextAuth() {
const { data: session, status } = useSession();
const [isConnected, setIsConnected] = useState(false);
const clientRef = useRef(null);
const connectingRef = useRef(false);
const connect = useCallback(async () => {
if (connectingRef.current || clientRef.current || !session?.accessToken) return;
connectingRef.current = true;
try {
const client = new CloudSignal({
tokenServiceUrl: 'https://auth.cloudsignal.app',
preset: 'desktop',
});
client.onConnectionStatusChange = setIsConnected;
client.onAuthError = () => { clientRef.current = null; };
await client.connectWithToken({
host: 'wss://connect.cloudsignal.app:18885/',
organizationId: process.env.NEXT_PUBLIC_CLOUDSIGNAL_ORG_ID!,
externalToken: session.accessToken as string,
});
clientRef.current = client;
} finally {
connectingRef.current = false;
}
}, [session?.accessToken]);
useEffect(() => {
if (status === 'authenticated') {
connect();
}
return () => {
clientRef.current?.destroy();
clientRef.current = null;
};
}, [status, connect]);
return { isConnected, client: clientRef.current };
}
Environment Variables
# .env.local
NEXT_PUBLIC_CLOUDSIGNAL_ORG_ID=your-org-uuid
NEXT_PUBLIC_SUPABASE_URL=your-supabase-url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
SSR Considerations
Since CloudSignal requires a browser environment:
- Always use
"use client"for components using the client - Check
typeof window !== 'undefined'if needed - Use dynamic imports for optional loading:
import dynamic from 'next/dynamic';
const LiveChat = dynamic(
() => import('@/components/LiveChat'),
{ ssr: false }
);
Troubleshooting
React StrictMode Double Connections
The hooks above include guards (connectingRef, mountedRef) that prevent duplicate connections in development mode.
Token Expiry
The useMQTTWithSupabase hook automatically handles token refresh by listening to onAuthStateChange.
Debug Mode
Enable detailed logging:
const mqtt = useMQTT({ debug: true });