# Tunneling Your Localhost With ngrok

I built a real-time voice translation Platform. Two people join a call, each picks the language they speak, and each hears the other translated live, in their own language. To actually test it, I needed a second person, on a second device, on a network that wasn't mine.

That's where everything about running a local project stopped being enough. This post is how I managed getting a friend, on his own Wi-Fi, into a call running on my laptop. No prior networking background assumed. I'll define every term the first time it shows up.

The problem

My app ran with next dev, and I opened it at [http://localhost:3000](http://localhost:3000) . It worked perfectly. I sent my friend that same address, and it went nowhere for him. That's expected, now let's see why it happened.  
  
What "localhost" actually means  
Every computer has a name that means - "myself": localhost. It's not a real address out on the internet. It's a computer talking to a program on the same computer. When my friend - Atharv, typed localhost:3000 into his own browser, his computer looked for something running on port 3000 on his machine, found nothing, and gave up. My laptop was never involved.  
So the fix isn't giving him the right localhost. It has to be basically an address that actually points at my laptop.  
  
Ports, properly explained  
A port is a number your computer uses to sort incoming traffic. One machine can run many programs at once: a dev server, a database, a chat app, whatever. They can't all listen on the same door(port), so each one picks a number. These numbers are not exactly present on laptop/machine but are hypothetical numbers that help the machine to organise and sort the incoming traffic. My Next.js dev server listens on port 3000. When a request arrives at my machine, the port number tells the operating system which program should get it.  
localhost:3000 reads as two instructions: go to this machine, then hand it to whatever's listening on 3000.  
  
Why my home network address didn't work either  
My laptop also has an address on my home Wi-Fi, something like 192.168.1.23. In theory, another device on the same Wi-Fi could reach that. A device on a different network, like Atharv's home internet, could not reach. Why? A reason worth understanding - home routers refuse traffic that shows up uninvited from the outside internet.  
Your router treats "outside requests nobody asked for" as suspicious by default, and blocks them. It is not a bug instead a deliberate security feature. The traditional fix is called port forwarding: you log into your router and tell it that anything arriving on port 3000 from outside, send it to this laptop. It works, but it means opening your home network to the internet and trusting your router's configuration screen to get it exactly right. I didn't want to do either. The extra rule that made this harder: microphones need HTTPS - This part is specific to anything using audio or video in the browser, and it's the main reason a voice app can't just skip straight to a "fix via updating router settings."

Browsers only allow microphone access on two kinds of pages: one served over HTTPS (an encrypted connection, shown as the padlock in the address bar), or localhost. A page opened at a plain address like http://192.168.1.23:3000 is neither.  
Even if I'd solved the router problem, Atharv's browser would have refused to turn his mic on. There was no way around this by tweaking router settings. I needed a real HTTPS address.

This is where comes the concept of tunnel. What a tunnel actually does?

A tunnel solves this by flipping the direction of the first connection.

Instead of someone from outside trying to reach my laptop (which my router blocks), my laptop reaches out to a tunnel provider's server and keeps that connection open. My laptop started the conversation, so my router has no objection. The tunnel provider then hands out a public HTTPS address. Anyone who opens that address is really talking to the tunnel provider's server, which passes the request down the open connection to my laptop, and passes the reply back the same way.

I used ngrok for this.

ngrok, step by step  
running ngrok http 3000 does three things:

1.  Starts a small local program that opens an outbound connection to ngrok's servers.
    
2.  Tells ngrok "forward whatever you receive to port 3000 on this machine."
    
3.  Prints a public HTTPS address, something like https://a1b2c3.ngrok-free.app.
    

I gave that address to Atharv. His browser requests went to that address, ngrok's servers received them, sent them down the tunnel to my laptop, my dev server answered, and the reply traveled back the same route.

Two terminal windows had to stay open the whole time: one running next dev, one running ngrok http 3000. Close either, and the address stops working immediately. This is temporary infrastructure by design, meant for exactly what I used it for: a live test with someone on another network, not a permanent way to host anything.

The bug that made every button silently do nothing  
Atharv opened the link. The page loaded, looked complete, and every button did absolutely nothing. No error in the browser console. Nothing.

Here's what was actually happening. Next.js sends the page in two pieces: the HTML first, so something shows up on screen fast, and the JavaScript after, which is what makes buttons and forms actually respond to clicks. The dev server has a built-in check that only trusts requests claiming to come from localhost. A request arriving through ngrok carries ngrok's own address in its headers, not localhost, so the dev server quietly refused to serve the JavaScript files. The HTML had already rendered fine, since that part isn't gated the same way, so the page looked done. It just couldn't do anything, because the code that makes it interactive never arrived.

The fix lives in next.config.ts:

`const nextConfig: NextConfig = {`

`allowedDevOrigins: [`

`"*.ngrok-free.app",`  
`"*.ngrok-free.dev",`  
`"*.ngrok.app",`  
`"*.ngrok.io",`

`],`

`};`

allowedDevOrigins tells the dev server which outside hosts to trust. I listed every ngrok domain ending I could find, since the free tier's exact domain has changed over time and I did not want to chase it again later. This setting is specific to recent versions of Next.js, so check your own version's docs before copying it blindly.

What actually travels through the tunnel

This matters, because it's the difference between "the tunnel is doing everything" and what's actually true.

Through ngrok: the page itself, its JavaScript, the request that gets a translation token from my server, and the small setup messages the two browsers exchange to find each other( who's in the call, what language each person speaks, how to reach one another directly).

Not through ngrok: the actual voice audio. Once the two browsers know how to reach each other, the audio travels directly between them, browser to browser, using a browser feature called WebRTC. My laptop and the tunnel only helped the two sides introduce themselves. They don't sit in the middle of the conversation itself.

What a tunnel doesn't fix  
A tunnel gets you a public address. It doesn't give you a server that's always running. The moment I closed my laptop, closed the terminal, or the tunnel session expired, the link died.  
Atharv testing the app is not the same as anyone, anytime, being able to open a working link. That needs an actual server that stays on, somewhere other than my laptop, which is a separate problem with its own tradeoffs.

Now lets see how to stay safe while the tunnel is open

While ngrok runs, my dev server is technically reachable from the public internet, so a few things mattered:

*   The actual API key for the translation service never leaves my server. The browser only ever gets a short-lived token, requested through my own backend route, so nothing sensitive is visible in the browser's dev tools.
    
*   Each call's link includes a long random code, not something guessable.
    
*   I closed the tunnel as soon as the test was done, rather than leaving it running.  
      
    Recap:
    
*   localhost means "this same machine." A port picks which program on it receives the traffic.
    
*   Home routers block incoming connections that were not asked for. Browsers refuse microphone access outside HTTPS or localhost.
    
*   A tunnel works by having your machine open the connection outward first, so the router allows it, then handing out a public HTTPS address that routes back through that connection.
    
*   ngrok http 3000 starts that tunnel and forwards traffic to port 3000.
    
*   The tunnel carries the page and the setup messages. It does not carry the actual audio, which goes directly between two browsers.
    
*   Skip allowedDevOrigins in a Next.js project and the page will load while doing absolutely nothing, with no error to tell you why.  
      
      
    What comes after the tunnel:  
    A tunnel is for testing with someone else while you're both still figuring out the app. It was never meant to be the way real users reach it later. That's a separate post, since deploying this kind of app to a real host brings up its own questions, like whether your server needs to stay a single running process or whether it can be split across many short-lived ones. I'll cover what I chose and why separately.
