Guides
Updated 2026-05-01

Building a Next.js App and Exposing It with Tunnelwise

How to set up a Next.js application, configure it to work through a Tunnelwise tunnel, and use the public URL for development and testing.

Next.js is one of the most widely used React frameworks for building web applications. Out of the box it gives you file-based routing, server-side rendering, API routes, and a fast development server with Hot Module Replacement (HMR).

By default, the dev server only accepts requests from localhost. That's fine for solo development, but as soon as you need to test on a different device, share a preview with a teammate, or work with any external service (payment hooks, OAuth callbacks, third-party widgets), you need a public HTTPS URL that points to your local machine. Tunnelwise does that in a few clicks.

This guide walks through creating a Next.js project, adding the configuration needed to run it behind a Tunnelwise tunnel, and verifying that HMR and WebSockets both work through the public URL.


Prerequisites

  • Node.js 18 or later
  • Tunnelwise installed on macOS

1. Create a Next.js Project

npx create-next-app@latest my-app
cd my-app

Accept the defaults or choose your preferred options (TypeScript, Tailwind, etc.). Once done, start the development server:

npm run dev

The app is now running at http://localhost:3000. Open it in the browser to confirm.


2. Create a Tunnel

  1. Open Tunnelwise on macOS.
  2. Create a new tunnel pointing to port 3000.
  3. Copy the public URL — it looks like https://your-tunnel.tunnelwise.io.

Open that URL in the browser. You'll likely see a Next.js error like:

Invalid Host header

or a 403 response. That's expected — Next.js rejects requests from origins it doesn't recognize. The next step fixes that.


3. Allow the Tunnel Origin in Next.js

Next.js 15+ validates the Origin header on incoming requests during development. Requests from *.tunnelwise.io will be blocked until you explicitly allow them.

Open next.config.ts (or next.config.js) and add allowedDevOrigins:

// next.config.ts
import type { NextConfig } from "next"

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

export default nextConfig

Restart the dev server after saving:

npm run dev

Reload your tunnel URL — the app should load normally now.


4. Content Security Policy (if you have one)

If your app sets a Content-Security-Policy header, the browser will block WebSocket connections to wss://*.tunnelwise.io unless that origin is listed in the connect-src directive. This affects HMR (which uses WebSockets) and any WebSocket features your app has.

Add the following CSP configuration to next.config.ts:

// next.config.ts
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 relaxed connect-src only applies when NODE_ENV === "development", so your production policy is unchanged.

Note: If you don't have a CSP configured yet, you can skip this step. Only add it if your app already sets Content-Security-Policy and WebSocket connections are failing through the tunnel.


5. Verify Hot Reload Through the Tunnel

With the tunnel running and the config applied:

  1. Open https://your-tunnel.tunnelwise.io in a browser — on any device on any network.
  2. Edit a component, for example change a heading in app/page.tsx:
// app/page.tsx
export default function Home() {
  return (
    <main>
      <h1>Hello from the tunnel</h1>
    </main>
  )
}
  1. Save the file. The browser connected to the tunnel URL should update automatically without a full reload — that's HMR working through the WebSocket connection Tunnelwise is forwarding.

To confirm the WebSocket connection is active, open the browser's DevTools → Network tab → filter by WS. You should see an active /_next/webpack-hmr connection.


6. Share the URL

The tunnel URL works on any device that has internet access — your phone, a teammate's laptop, a tablet. No VPN or local network required.

Your Tunnelwise URL stays stable as long as the tunnel is not inactive for more than 5 days, so you don't need to update any bookmarks or shared links during a development session.


Common Issues

The app doesn't load through the tunnel

Make sure allowedDevOrigins includes "*.tunnelwise.io" in next.config.ts and that you restarted the dev server after the change. A config change without a restart has no effect.

HMR doesn't work through the tunnel

Open DevTools → Network → WS and check whether the /_next/webpack-hmr WebSocket connection is established. If it's missing or failing:

  • Confirm the tunnel is still active in Tunnelwise.
  • If you have a CSP, verify that wss://*.tunnelwise.io is in the connect-src directive.
  • Check the browser console for a blocked connection error.

Requests through the tunnel return 403

This usually means the origin check is still blocking the request. Double-check the allowedDevOrigins value — it must be "*.tunnelwise.io" (with the wildcard), not just "tunnelwise.io".