Concepts

Pipes & permissions

Every user who connects gets a set of pipe permissions. You define these in the object returned by your authorize function. Each pipe can grant two abilities: subscribe (receive events) and publish (send events).

In a team chat, every member should be able to send and receive messages. Give everyone both permissions on the pipe:

authorize: async (req) => {
  const { session } = await getAuth(req);
  if (!session) return null;

  return {
    userId: session.userId,
    pipes: {
      'team-chat': { subscribe: true, publish: true },
    },
  };
},

Sometimes you need to send events to a specific user — a notification, a status update, something only they should see. Use a dynamic pipe name with their user ID:

return {
  userId: session.userId,
  pipes: {
    [`user-${session.userId}`]: { subscribe: true },
  },
};

Now your server can publish to user-abc123 and only that user receives it. Other users can't subscribe to someone else's pipe because it's not in their permissions.

For things like live dashboards, announcements, or system alerts, you might want users to receive events but not send them. Only grant subscribe:

return {
  userId: session.userId,
  pipes: {
    announcements: { subscribe: true },
  },
};

Most apps need a mix. Here's what a team collaboration app might look like:

authorize: async (req) => {
  const { session } = await getAuth(req);
  if (!session) return null;

  const teamId = await getTeamId(session.userId);

  return {
    userId: session.userId,
    pipes: {
      [`team-${teamId}`]: { subscribe: true, publish: true },
      [`user-${session.userId}`]: { subscribe: true },
      'announcements': { subscribe: true },
    },
  };
},

Three pipes, three different access patterns:

  • team-abc — everyone on the team can send and receive (chat, typing indicators, live cursors)
  • user-xyz — only this user receives (notifications, direct messages from the server)
  • announcements — all users receive, only the server publishes (maintenance alerts, feature launches)

If a user tries to subscribe or publish to a pipe that isn't in their permissions, the API ignores the request.

When a user's permissions expand (they create a room, join a channel, get promoted to a new role), call refreshPipeAuth() from the client. This re-runs your authorize function and updates the connection's permissions without disconnecting:

import { refreshPipeAuth } from '@/lib/realtime-client';

async function createRoom(name: string) {
  const room = await createChatRoom(name);

  // permissions just changed — tell Hotpipe to pick up the new pipes
  await refreshPipeAuth();

  return room;
}

When a user should lose access (kicked from a room, downgraded, account suspended), call pipeAdmin.revoke() from your server:

import { pipeAdmin } from '@/lib/realtime-admin';

async function removeFromRoom(userId: string, roomId: string) {
  await db.rooms.removeUser(userId, roomId);
  await pipeAdmin.revoke(userId, `room-${roomId}`);
}

Revocations take effect immediately — the user is unsubscribed and receives a system event, even if they're currently connected. If the user is offline, the revocation is enforced when they reconnect.

To update your UI when access is revoked, pass the onPipeRevoked and onAllPipesRevoked callbacks when creating the client:

export const { PipeProvider, usePipe, refreshPipeAuth } = createPipeClient({
  events: realtimeEvents,
  onPipeRevoked: (pipe) => {
    // remove channel from sidebar, show toast, etc.
  },
  onAllPipesRevoked: () => {
    // redirect to login, show modal, etc.
  },
});

See the client API reference and server API reference for full details on these functions.

Was this page helpful?