Development with a tunnel

Telegram and Slack cannot reach localhost. Nuxt dev --tunnel fixes that, and one surprise about Cloudflare quick tunnels will cost you an evening otherwise.

http://localhost:3000 exists on your machine and nowhere else. Telegram, Slack, GitHub and Stripe all live on the internet, and the internet has never heard of your laptop.

A tunnel gives you a public address that forwards to your dev server. Nuxt ships with one, and it costs nothing and needs no account.

Copy this

Start Nuxt with a tunnel

terminal
pnpm dev --tunnel

It prints two addresses:

  ➜ Local:    http://localhost:3000/
  ➜ Network:  https://polite-badger-arrives.trycloudflare.com/

The second one works from anywhere in the world. It is temporary and it changes every time you restart.

Point the service at it

For Telegram, once:

server/api/register.get.ts
export default defineEventHandler(async () => {
  return telegram.setWebhook('https://polite-badger-arrives.trycloudflare.com')
})

For everything else, paste the address plus the route into that service's own settings page:

https://polite-badger-arrives.trycloudflare.com/api/_pigeon/webhook

Keep the page on localhost

Open your app at http://localhost:3000, not through the tunnel.

It works. Messages arrive, and your frontend updates.

Why the last step matters

This is the surprise, and it is the reason this page exists.

A Cloudflare quick tunnel does not deliver server sent events. The connection opens, stays open, and nothing ever comes through. From the outside it looks exactly like a broken server: your browser says connected, your logs say the messages arrived, and the page stays empty.

It is not a bug and it will not be fixed by waiting. From the cloudflared maintainers, on issue #1449:

It works as expected with named tunnels. The reason it doesn't work with quick-tunnels is because we have some guardrails due to it being a demo product.

And a day later, in the same thread:

We will try to have SSE work on quick tunnels, but I can't give an ETA.

The same finding is in issue #199 from 2020, closed without a fix.

The way out costs nothing: the tunnel is only needed so the service can reach you. Short incoming requests pass through it perfectly. Your own browser does not need it at all, so open the page on localhost and let the tunnel do the one job it is good at.

Through the tunnelOn localhost
Telegram delivering an update❌ it cannot reach you
GitHub delivering a push event
Your page receiving the live streamsilently empty
With a named tunnel, one tied to a Cloudflare account, streams work. So does production behind nginx, Caddy, Vercel or Cloudflare's normal proxy. The limit is specific to the free throwaway tunnels.

What will cost you an hour

  • The address changes on every restart. Telegram remembers the old one, so after a restart you register again, or nothing arrives and nothing errors.
  • A proxy closes a connection nothing flows through. Cloudflare after 100 seconds with a 524, an AWS load balancer after 60 by default. The module sends an invisible heartbeat every 30 seconds so the stream survives an idle night.
  • --tunnel needs a download on first use. Nuxt fetches cloudflared once. On a bad connection the first start looks like it is hanging when it is downloading.
  • Anyone with the address can reach your dev server while it runs. It is a public url. That is the whole point, and it is worth remembering when you leave it open.

When you are past experimenting

For anything that has to survive a restart, a named tunnel is worth the five minutes:

terminal
cloudflared tunnel login
cloudflared tunnel create nuxt-pigeon
cloudflared tunnel route dns nuxt-pigeon dev.example.com
cloudflared tunnel run --url http://localhost:3000 nuxt-pigeon

You get a stable address, so you register the webhook once and never again, and server sent events work.

Read more