← Back to blog
INTEGRATION7 min read

Add Realtime to a Next.js App with NoLag

HB
Henco Burger
July 30, 2026

This post was written for NoLag Cloud, the hosted service, which was retired in October 2026. To run NoLag yourself, start with the self-hosting guide.

Next.js renders on the server and hydrates in the browser, so the one rule for adding realtime is simple: the realtime connection belongs in a client component. This guide shows the clean pattern, including a server route that hands the browser a token so your project key never ships to the client.

1. Install the SDK

npm install @nolag/js-sdk

2. Mint a token on the server

Never put a project key in browser code. Add a route handler that creates a token server-side and returns it. The NOLAG_API_KEY (an nlg_live_... key) stays in your server environment.

// app/api/nolag-token/route.ts
import { NextResponse } from 'next/server'

export async function GET() {
  const res = await fetch('https://api.nolag.app/v1/actors', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.NOLAG_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ name: 'Web Client', actorType: 'user' }),
  })

  const actor = await res.json()
  return NextResponse.json({ token: actor.accessToken })
}

For production, prefer short-lived client tokens minted from your project signing keys so browser sessions get scoped, expiring credentials.

3. Connect from a client component

'use client'
import { useEffect, useRef, useState } from 'react'
import { NoLag } from '@nolag/js-sdk'
import type { RoomContext } from '@nolag/js-sdk'

// Create the app and its `general` room first. The slug you get back has a
// random suffix (for example `chat-a3f9`), and that suffixed slug is what
// setApp() takes.
const APP_SLUG = process.env.NEXT_PUBLIC_NOLAG_APP_SLUG!
// Your broker, for example ws://localhost:8080/ws when you run kraken yourself
const KRAKEN_URL = process.env.NEXT_PUBLIC_NOLAG_URL!

type Message = { text: string }

export default function Chat() {
  const [messages, setMessages] = useState([] as Message[])
  const roomRef = useRef(null as RoomContext | null)

  useEffect(() => {
    let client: ReturnType<typeof NoLag> | undefined
    let active = true

    ;(async () => {
      const { token } = await fetch('/api/nolag-token').then((r) => r.json())
      if (!active) return

      client = NoLag(token, { url: KRAKEN_URL })
      const room = client.setApp(APP_SLUG).setRoom('general')
      room.on('message', (data) => setMessages((m) => [...m, data as Message]))
      // Subscribe on every connect: the handler runs after each reconnect too
      client.on('connect', () => room.subscribe('message'))
      roomRef.current = room
      await client.connect()
    })()

    return () => {
      active = false
      roomRef.current = null
      client?.disconnect()
    }
  }, [])

  const send = (text: string) => {
    const msg: Message = { text }
    roomRef.current?.emit('message', msg)
    // Publishers never receive their own messages, so append it locally
    setMessages((m) => [...m, msg])
  }

  return (
    <div>
      <ul>
        {messages.map((m, i) => (
          <li key={i}>{m.text}</li>
        ))}
      </ul>
      <button onClick={() => send('Hello!')}>Send</button>
    </div>
  )
}

The active flag guards against React running effects twice in development and against a connection resolving after the component unmounts. The send function appends the message to local state as well as emitting it: NoLag never delivers a message back to the actor that published it, so the sender's own text would otherwise never appear on screen.

Why the client-component rule matters

The NoLag SDK runs in Node as well as in the browser, so this is not about where the code can execute. It is about lifecycle. A server component renders once, has no mount or unmount, and its output is serialised and hydrated in the browser, so there is nowhere for a long-lived connection to live and nothing it receives would survive hydration. Keep the NoLag(...) call inside useEffect (or an event handler) in a 'use client' file and the connection opens after hydration and closes on unmount, which avoids hydration mismatches and duplicated connections.

Next steps