Back to Blog
Product

WebSocket Support in Tunnelwise: Simplifying Real-Time Testing on macOS

Gonzalo Buszmicz
Gonzalo Buszmicz
Software Engineer
Apr 23, 2026
7 min read
WebSocket Support in Tunnelwise: Simplifying Real-Time Testing on macOS

Testing real-time apps locally has always had one annoying step: your WebSocket server runs on localhost, but the client — a mobile device, a co-worker's browser, a staging environment — can't reach it.

Until now, Tunnelwise only forwarded HTTP traffic. That meant developers building WebSocket-based features had to fall back to ngrok or leave testing until deployment. With v1.3.0, Tunnelwise tunnels WebSocket connections too, so you can expose a local Socket.IO server or a Next.js dev server and test against it from anywhere.

This post shows how to set that up with two common stacks: Socket.IO (Node.js) and Next.js.


How WebSocket Tunneling Works

A WebSocket connection starts as an HTTP request — the client sends an Upgrade: websocket header, the server agrees, and the connection switches protocols. Tunnelwise now handles that upgrade correctly, forwarding both the initial HTTP handshake and the subsequent WebSocket frames to your local server.

No extra configuration is needed in Tunnelwise itself. Create a tunnel on the port your app is listening on, and WebSocket connections through that URL will just work.


Testing a Socket.IO App

Socket.IO is one of the most common libraries for WebSocket-based features: live notifications, chat, collaborative editing, multiplayer.

Here's a minimal server:

// server.js
import { createServer } from "http"
import { Server } from "socket.io"

const httpServer = createServer()
const io = new Server(httpServer, {
  cors: { origin: "*" },
})

io.on("connection", (socket) => {
  console.log("client connected:", socket.id)

  socket.on("message", (data) => {
    io.emit("message", data) // broadcast to all clients
  })

  socket.on("disconnect", () => {
    console.log("client disconnected:", socket.id)
  })
})

httpServer.listen(3001, () => console.log("listening on :3001"))

Start the server, then open Tunnelwise and create a tunnel on port 3001. You'll get a public URL like https://abc123.tunnelwise.io.

In your client, connect to that URL instead of localhost:

import { io } from "socket.io-client"

const socket = io("https://abc123.tunnelwise.io")

socket.on("connect", () => {
  console.log("connected:", socket.id)
})

socket.on("message", (data) => {
  console.log("received:", data)
})

Socket.IO's transport negotiation handles the WebSocket upgrade automatically. The tunnel is transparent from the library's perspective.


Testing a Next.js App

Next.js adds some configuration requirements when running behind a tunnel, mostly around allowed origins and Content Security Policy.

Allowing the Tunnel Origin

Next.js 15+ validates the Origin header on requests during development. Without explicit configuration, requests coming from *.tunnelwise.io will be blocked.

Add the tunnel domain to allowedDevOrigins in next.config.js:

// next.config.js
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  allowedDevOrigins: [
    "*.tunnelwise.io",
  ],
};

export default nextConfig;

This is enough for most cases. If you have a Content Security Policy configured, you'll also need to add the wss://*.tunnelwise.io origin to the connect-src directive so the browser allows outgoing WebSocket connections:

// next.config.js
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  allowedDevOrigins: [
    "*.tunnelwise.io",
  ],
  async headers() {
    return [
      {
        source: "/(.*)",
        headers: [
          {
            key: "Content-Security-Policy",
            value:
              process.env.NODE_ENV === "development"
                ? "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' ws: wss: ws://localhost:* wss://localhost:* https://*.tunnelwise.io wss://*.tunnelwise.io; font-src 'self';"
                : "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' ws: wss:; font-src 'self';",
          },
        ],
      },
    ];
  },
};

export default nextConfig;

The CSP changes only apply in development (NODE_ENV === "development"), so your production policy stays unchanged.

After updating the config, start next dev and create a tunnel on port 3000. Share the tunnel URL with a device or team member, and WebSocket-dependent features — Next.js Fast Refresh included — will work through the tunnel.


Common Pitfalls

Socket.IO falls back to polling. If the WebSocket connection fails, Socket.IO silently falls back to HTTP long-polling. Check the browser's Network tab and filter by WS to confirm a WebSocket connection was actually established. If you only see HTTP requests, the upgrade is failing somewhere.

The Next.js origin check blocks the request. If you see a 403 or a CORS error when accessing your app through the tunnel, double-check that allowedDevOrigins includes *.tunnelwise.io and that you've restarted the dev server after changing next.config.js.

wss:// vs ws://. Traffic through the tunnel is encrypted — use wss:// (not ws://) when connecting to a tunnelwise.io URL. Socket.IO infers this from the https:// scheme automatically, but if you're constructing WebSocket URLs manually, make sure the protocol matches.


What to Test

A few things worth verifying once you have a tunnel running:

  • Connection and disconnection events fire correctly
  • Messages are delivered in both directions without corruption
  • Reconnection works after the client drops and re-establishes the connection
  • Your app handles the tunnel URL the same way it handles localhost — no hardcoded localhost references that break in remote testing

Alternatives

ngrok and Cloudflare Tunnel both support WebSockets as well. If you're already using one of those, no reason to switch. Tunnelwise is a macOS-native option if you prefer not to manage a CLI session.

Ready to try Tunnelwise?

Start tunneling your localhost to the world in seconds