Any language

There are exactly two things a backend has to do, and neither needs an SDK: sign a subscription, and publish an event. One is an HMAC, the other is an HTTP POST. Everything below is a few lines, and every signing snippet on this page is executed by our test suite against a real gateway before it ships.

On the frontend there is nothing to port at all — @socketly/client touches no browser-only global, so the same package runs in a browser, in React Native and Expo, and anywhere else JavaScript runs.

Authorize a subscription

Your backend already knows who its user is. This is where it says so — for private- and presence- channels, the client asks your endpoint and your endpoint signs. public- channels skip this entirely.

// app/api/socketly/auth/route.ts — Next.js
import { createHmac } from 'node:crypto';

export function socketly_authorize(secret, socketId, channel, userData) {
  const appId = 'app_' + secret.split('_')[2];
  let channelData = null;
  let payload = socketId + ':' + channel;

  if (channel.startsWith('presence-')) {
    channelData = JSON.stringify(userData);
    payload += ':' + channelData;
  }

  const signature = createHmac('sha256', secret).update(payload).digest('hex');
  const result = { auth: appId + ':' + signature };
  if (channelData) result.channel_data = channelData;
  return result;
}

export async function POST(request: Request) {
  const { socket_id, channel_name } = await request.json();

  // You decide who may subscribe. You already know who your user is.
  const user = await getCurrentUser();
  if (!user) return new Response('Unauthorized', { status: 401 });

  return Response.json(
    socketly_authorize(process.env.SOCKETLY_SECRET, socket_id, channel_name, {
      user_id: user.id,
      user_info: { name: user.name },
    }),
  );
}

Watch out — You probably want `@socketly/server` instead — it is this function plus a publish client, and it runs on Vercel Edge and Cloudflare Workers where `node:crypto` does not.

Return the string you signed, not the object. The gateway verifies against the exact channel_data bytes the client sends back, so key order, slash escaping and separator spacing are all free — Go sorts map keys, PHP escapes slashes, and both verify fine. What does not verify is signing one serialization and then handing your framework the object to serialize again: that second pass is free to differ, and then nothing matches. Build the string once and use it twice, as every snippet above does.

Publish an event

A POST with your secret key as a bearer token. The app id is the middle segment of that key, so there is no second value to keep in step with it.

const appId = 'app_' + process.env.SOCKETLY_SECRET.split('_')[2];

await fetch(`https://api.socketly.co/v1/apps/${appId}/events`, {
  method: 'POST',
  headers: {
    'content-type': 'application/json',
    authorization: `Bearer ${process.env.SOCKETLY_SECRET}`,
  },
  body: JSON.stringify({
    channels: ['presence-room-42'],
    event: 'new-message',
    data: { text: 'Shipped.' },
  }),
});

On the frontend

@socketly/react exists because hooks are pleasant, not because React is required. Everything else uses @socketly/client directly, and it is the same object underneath.

'use client';
import { useChannel, usePresence } from '@socketly/react';

export function Room({ roomId }: { roomId: string }) {
  const channel = `presence-room-${roomId}`;

  const { subscribed } = useChannel(channel, {
    'new-message': ({ data }) => append(data.text),
  });
  const { members, count } = usePresence(channel);

  if (!subscribed) return <p>Connecting…</p>;
  return <p>{count} online</p>;
}

What is not on this page

A signing snippet we have not run. Every language above is executed by check-signatures.mjs on each build — it subscribes to a real presence channel with that language's own output and asserts the gateway accepts it — and a language whose runtime is missing skips out loud rather than passing quietly. If yours is not here, the construction is short enough to port from any of them: HMAC-SHA256 over socketId:channel, or socketId:channel:channelData for presence, hex-encoded, prefixed with your app id and a colon.