WebSocket

Harpia provides a powerful WebSocket routing system built on top of Bun’s blazing-fast native WebSocket implementation. It allows you to easily create real-time endpoints while keeping your codebase organized.

Defining WebSocket Routes

You can define a WebSocket route directly on the harpia instance using app.ws(), or on a Router instance using router.ws().

The ws() method takes a path and a handlers object.

import harpia from "@harpiats/core";

const app = harpia();

app.ws("/chat", {
  open(ws) {
    console.log("Client connected!");
  },
  message(ws, message) {
    console.log(`Received: ${message}`);
  },
  close(ws, code, reason) {
    console.log("Client disconnected");
  }
});

Available Handlers

The handlers object accepts the same lifecycle events as Bun’s native WebSocket:

  • open(ws): Called when a connection is established.
  • message(ws, message): Called when a message is received from the client.
  • close(ws, code, reason): Called when the connection is closed.
  • drain(ws): Called when the connection is ready to send more data.
  • error(ws, error): Called when an error occurs.

Automatic Broadcasting

A unique feature of Harpia’s WebSocket implementation is automatic broadcasting.

By default, when a message is received on a route, Harpia automatically broadcasts that exact message to all other currently active connections on the server.

app.ws("/room", {
  message(ws, message) {
    // 1. You can process, log, or save the message here
    console.log(`User sent: ${message}`);
    
    // 2. Harpia automatically broadcasts 'message' to everyone else connected!
  }
});

If you need to send a specific message back only to the sender, you can use ws.send():

app.ws("/echo", {
  message(ws, message) {
    ws.send(`You said: ${message}`);
  }
});

Accessing Connection Data

During the HTTP-to-WebSocket upgrade process, Harpia automatically extracts useful information and attaches it to ws.data. You can easily add TypeScript definitions for any extra data you plan to attach or expect on the WebSocket connection.

Custom Types

You can pass a generic type to app.ws<T>() or router.ws<T>() to strongly type the ws.data object. This is especially useful when your WebSocket connection carries specific payload data or when you manually attach data after authentication.

interface MyWebSocketData {
  cookies: Record<string, string>;
  userId?: number;
}

app.ws<MyWebSocketData>("/chat", {
  open(ws) {
    // Strongly typed access
    const cookies = ws.data.cookies;
    
    if (!cookies.session_id) {
      // Reject unauthorized connections immediately
      ws.close(1008, "Unauthorized");
      return;
    }
    
    // Attach custom data to the socket for later use
    ws.data.userId = 123;
  },
  message(ws, message) {
    // TypeScript knows ws.data.userId exists!
    console.log(`User ${ws.data.userId} sent: ${message}`);
  }
});

Sessions and Authentication

If you are using the Harpia Session class, you can retrieve the session data directly from the WebSocket connection to verify if the user is authenticated before allowing them to use the socket.

import { Session } from "@harpiats/core";

const session = new Session();

interface ChatData {
  username?: string;
}

app.ws<ChatData>("/profile-updates", {
  async open(ws) {
    // Attempt to retrieve the session
    const userData = await session.fromWebSocket(ws);
    
    // Check if the user is logged in
    if (!userData) {
      return ws.close(1008, "Unauthorized");
    }
    
    // Store data in the typed ws.data for use in other handlers
    ws.data.username = userData.username;
    ws.send(`Welcome back, ${ws.data.username}!`);
  }
});

Token Authentication (JWT)

Because the standard browser WebSocket API does not support sending custom HTTP headers (like Authorization), authenticating via JWT requires a different approach. There are three main ways to handle this in Harpia, ordered by security best practices:

The most secure approach for token-based authentication is to establish the connection unauthenticated, but require the client to send their token in the very first message. If the server does not receive a valid token immediately, it drops the connection.

app.ws("/chat", {
  open(ws) {
    // Optionally set a timeout to drop the connection if no auth message arrives
    ws.data.authTimeout = setTimeout(() => {
      if (!ws.data.isAuthenticated) ws.close(1008, "Auth Timeout");
    }, 5000); // 5 seconds
  },
  message(ws, message) {
    // Parse the incoming message
    const data = JSON.parse(message);
    
    // First message must be the authentication token
    if (!ws.data.isAuthenticated) {
      if (data.type === "auth" && verifyToken(data.token)) {
        ws.data.isAuthenticated = true;
        clearTimeout(ws.data.authTimeout);
        ws.send("Authentication successful!");
        return;
      }
      return ws.close(1008, "Invalid Token");
    }
    
    // Handle normal messages for authenticated users
    console.log(`Received secure message: ${data.text}`);
  }
});

2. HttpOnly Cookies

Just like Sessions, if your JWT is stored in an HttpOnly and Secure cookie, the browser will automatically send it during the initial WebSocket handshake. You can read it directly from ws.data.cookies inside the open handler, keeping the token completely hidden from client-side scripts and URLs.

A common, but insecure, workaround is passing the token in the URL: ws://localhost/chat?token=123. While Harpia allows you to easily parse this from ws.data.url, you should avoid this in production.

Security Risk

Passing sensitive tokens in Query Parameters is considered a bad security practice. Even though WSS (WebSocket Secure) encrypts the traffic, the full URL (including the token) can be exposed in:

  • Server and proxy logs (Nginx, Apache)
  • Network monitoring tools and APMs
  • The Referer header if the user clicks an external link
  • The browser’s history

Performance

Because Harpia leverages Bun’s native WebSockets, each connection uses very little memory. You can easily handle tens of thousands of concurrent connections on a single server instance.