Docs

Next.js

WebSockets in a Next.js app, with a token route handler on the server and live client components in the browser.

A Next.js app has both halves of a realtime integration in one repo: a route handler that mints tokens with your API key, and client components that hold the WebSocket. This page wires them together with the app router.

The token route handler

Your API key stays on the server, in an env var without the NEXT_PUBLIC_ prefix so it’s never bundled for the browser:

// app/api/realtime/token/route.ts
import { createJwt } from '@foony/realtime';

export async function GET() {
  const userId = await requireUser();  // Your session check

  const token = await createJwt(
    {
      clientId: userId,
      capability: { 'chat:*': ['subscribe', 'publish', 'presence', 'history'] },
    },
    { key: process.env.FOONY_API_KEY! },
  );

  return new Response(token);
}

createJwt signs in-process with no network call, so this route is fast enough to sit on the request path. Scope the capability to what the signed-in user may actually do, per Authentication and capabilities.

The shared client

// lib/realtime.ts
import { Realtime } from '@foony/realtime';

export const realtime = new Realtime({
  authCallback: async () => {
    const response = await fetch('/api/realtime/token');
    return response.text();
  },
});

Module scope is safe here even though Next.js also evaluates client modules during server rendering, because constructing a Realtime doesn’t open a socket. The connection happens on first use, and first use only happens in effects, which only run in the browser.

A live client component

The SDK needs the browser, so components that subscribe are client components:

// app/dashboard/live-orders.tsx
'use client';

import { useEffect, useState } from 'react';
import { realtime } from '@/lib/realtime';

export function LiveOrders() {
  const [orders, setOrders] = useState<Order[]>([]);

  useEffect(() => {
    const channel = realtime.channels.get('chat:orders');
    return channel.subscribe((message) => {
      setOrders((current) => [message.data as Order, ...current]);
    });
  }, []);

  return <ul>{orders.map((order) => <li key={order.id}>{order.summary}</li>)}</ul>;
}

Server components can render this component anywhere. The subscription starts on the client after hydration. The React patterns, one client, effects that return the unsubscribe function, presence as state, are covered in the React quickstart.

Publishing from server code

Server actions and route handlers publish over REST without holding a socket:

import { Rest } from '@foony/realtime';

const rest = new Rest({ key: process.env.FOONY_API_KEY! });
await rest.channels.get('chat:orders').publish('created', order);

This is the natural shape for Next.js: mutations happen in server code, the publish rides the mutation, and every open dashboard updates.

Deployment notes

Set FOONY_API_KEY in your host’s env settings. Everything here works on serverless hosts like Vercel, because the server side only makes HTTPS calls. The WebSocket lives in your users’ browsers pointed at realtime.foony.io, not in your Next.js server, so there’s no long-lived connection for your host to keep alive.