How a web app on a personal computer became reachable from the internet without opening a router port
The goal. Let visitors reach an app on a personal computer at home, under a name such as kanbanassist.vishnuvardhan.io. The router stays unchanged.
The approach. cloudflared on the computer opens one outbound, encrypted connection to Cloudflare. Visitor requests return down that same connection. Nothing is forwarded on the router.
/api continues to Spring. Ollama is never routed to the internet.Every device at home has a private address, such as 192.168.1.20. The router shares one public address among them. When the computer opens a connection, the router rewrites the source address and records the mapping. This is network address translation (NAT).
Take one request. The computer connects to 93.184.216.34 on port 443, the standard HTTPS port, from local port 51514. The router records that mapping. A reply that matches it returns to 192.168.1.20. A packet that matches no mapping is discarded, so a visitor's first packet never reaches the computer.
The design had to leave the router unchanged, serve visitors by name over HTTPS, keep the model private, and serve the page and the API from one origin. Port forwarding sends one port from the router to the computer. It works, and it is the usual answer. But the computer then answers anyone who scans the address, needs a certificate for a domain name, and breaks when the address changes. Many residential providers also use carrier-grade NAT, which shares one address among many customers. Port forwarding cannot work on those connections.
A tunnel reverses who opens the connection. The computer calls out, and visitors ride that call.
| Option | Router change | Works for public visitors | Main cost |
|---|---|---|---|
| Port forwarding | Yes | Yes | The computer is exposed. Certificates and address changes need upkeep |
| Cloud hosting | No | Yes | Monthly cost. The data and the model leave the home |
| Cloudflare Tunnel | No | Yes | Trust in Cloudflare. The computer must stay online |
GitHub Pages could host the static page but not the API. The API would still need a tunnel, and the two would sit on different origins. Cloud hosting breaks the privacy requirement. The tunnel was the only option that met every requirement.
This setup uses two parts of Cloudflare Tunnel. The first is cloudflared, a connector program installed as a Windows service on the computer. The second is tunnel routing: a public hostname that Cloudflare maps to a local service. Here the route points the hostname at the Vite preview server on 127.0.0.1:4173. Vite forwards /api to Spring, so one route serves both.
cloudflared opens an encrypted session outward to Cloudflare. The router already tracks outbound connections, so it needs no change. When a visitor arrives, Cloudflare sends the request down the session the computer opened.
The session has three layers. TCP moves the bytes. TLS, the encryption behind HTTPS, protects them. A tunnel protocol carries many requests over one connection. The connector opens several such connections to different Cloudflare data centers, so one failing path does not take the site down. This article does not test failover.
Three systems are involved. The edge is Cloudflare's server nearest each visitor. It receives every connection before the computer does.
| System | Role | What it had to do |
|---|---|---|
| Cloudflare | Public entry point | Terminate HTTPS, match the hostname to a route, and send the request down the open session |
| Windows | Runs cloudflared as a service that survives reboots | Registering the service requires administrator rights. No inbound firewall rule was added |
| Spectrum home router | Network address translation and connection tracking | Nothing changed. No port was forwarded |
Loading the page and sending a chat message share their first hops. They differ at Vite. Loading the page ends there. A chat message continues to Spring.
The page and the API share one hostname, so the browser treats the call to /api as same-origin. Browsers block cross-origin calls unless the server allows them through CORS, which needs matching headers on both sides. One hostname avoids that. The cost is that Vite's /api rule must be correct.
A route is as wide as what sits behind it. The page route exposes the page and, through Vite, the API. A route straight to Spring would skip Vite's host check, which is why the setup keeps one route. Still open, in priority order:
https:// for a plain HTTP service. The public site works, but the setting should be checked before other services rely on it.Each step ends with a check. Do not start the next step until it passes. Tokens and identifiers are masked in every screenshot.
server:
port: 8090
allowedHosts. Without that entry, Vite returns 403 for the public address.
preview: {
host: "127.0.0.1",
port: 4173,
strictPort: true,
allowedHosts: ["kanbanassist.vishnuvardhan.io"],
},
vite.config.ts. The /api rule forwards to Spring.npm run build, then npm run preview. Open 127.0.0.1:4173 and send a chat message. If this fails, the problem is the app, not the tunnel.
Test-NetConnection 127.0.0.1 -Port 8090. The result must show TcpTestSucceeded : True. Command Prompt does not have this command.
kanbanassist, domain vishnuvardhan.io, service HTTP, URL 127.0.0.1:4173.
The React build produces static files: index.html, JavaScript and CSS. Any web server can host them, and no Node.js process runs at runtime. The API address is relative, so /api works on any host that serves both parts. A client-side route such as /board exists only in the browser, so the server must return index.html for it. Otherwise a direct visit returns a 404.
Each symptom points to one layer. Read the source of a message before searching its text, and run the checks in order.
| What you see or run | Where it comes from | What it means |
|---|---|---|
| Tunnel shows Healthy | Cloudflare dashboard | The session to Cloudflare is open |
| 502 at the public address | Cloudflare | The request arrived, but nothing listened at the target |
| 403 at the public address | Vite | The hostname is missing from the allowedHosts list |
404 JSON from port 8090 at / | Spring | Spring is running. The API has no root page, so this is expected |
ERR_CONNECTION_REFUSED on 4173 | The local browser | Nothing listens on 4173 on this machine. The tunnel is not involved |
Test-NetConnection to port 8090 on this machine | PowerShell | TcpTestSucceeded : True means something listens on 8090 |
curl.exe -v sent to the local port | Command line | The status line and headers the service returns |
nslookup on the public hostname of your site | DNS | Cloudflare addresses, never the home network |
| Cloudflare Live logs | Cloudflare dashboard | The status codes visitors receive |
Most failures were between components, not in the application.
| Symptom | Cause | Fix |
|---|---|---|
| Installer does nothing | Windows blocks downloaded files | Unblock it in Properties |
| Access denied on service install | A standard prompt has no administrator rights | Run Command Prompt as administrator |
Test-NetConnection is not found in Command Prompt | Typed into Command Prompt | Run it in PowerShell |
| Route points at port 3000 | No process listens there | Point the route at 4173, and set strictPort so Vite never moves to another port |
TcpTestSucceeded : False on 4173 | The preview server had stopped | Restart npm run preview |
| Form refuses to create the tunnel | The name field was empty | Enter a name |
| Testing on port 5173 | The development server is not what the tunnel serves | Test the preview port, 4173 |
Visitors saw a 403 at the public address. Vite produced it, not Cloudflare or Spring, and the message named the setting to change: preview.allowedHosts. Identify which component produced an error before searching for its text.
The public chat failed, so the checks ran one hop at a time. Ping to the computer succeeded. The TCP test on 4173 failed. Port 8090 returned Spring's JSON 404 at the root, which showed Spring was running. The preview server had stopped. Restarting it fixed the problem.
During testing I created a second route to the API hostname. It sent requests straight to Spring, bypassing Vite's host check and the /api rule. The masked row below is that route. I deleted it. Review the Routes table after every change.
https:// service address for a plain HTTP server, which is covered in the known gaps.The chat stopped responding on the preview server. My first explanation, a missing proxy rule, was wrong. Vite already applies server.proxy when preview.proxy is not set. I then made the preview block explicit, and the chat works through the tunnel. I did not isolate which change fixed it, so I do not claim a cause.
What it gives you. The router is untouched, and no inbound port is open. A changing home address does not break the site, because the connection is made outward. Cloudflare presents the certificate for the hostname, so nothing renews on the computer. Deleting the route stops public access at once. The setup used Cloudflare's free tier.
What it costs.