CloudSignal Docs
Getting StartedQuickstarts

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

PieceRole
Server routeMints MQTT credentials for the signed-in user
React hookManages the SDK client lifecycle
Live componentSubscribes and publishes from the browser

Install dependencies

npm install @cloudsignal/mqtt-client

Set 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_id

Get 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

On this page