Reaching a Home-Hosted App From Anywhere with Tailscale
demonstrates: Secure remote access to a self-hosted service via a private mesh, chosen deliberately over public tunnels because this build has no authentication of its own.
This guide gets you to one outcome: opening your TaskBoard from a phone on cellular, moving a task, and hearing the receipt print at home, without opening a single port to the internet. It assumes you can install software on the home PC and the phone and edit one app setting. TaskBoard’s API and the USB printer it drives both stay home; only a private, encrypted mesh reaches in.
The problem
- The board’s data lives in a FastAPI + SQLite server, and that server also drives a USB thermal printer. The printer can’t leave home, so the API can’t either.
- The goal is to open the board and move tasks (which print at home) from a phone on cellular, without exposing an unauthenticated API to the public internet.
Why a private mesh (briefly)
- This build of TaskBoard has no authentication at all: no login, no tokens, no accounts. The API answers any request that reaches it, which is fine on a private LAN and unsafe on a public URL. That absence is exactly why the private mesh is load-bearing rather than a convenience.
- A private WireGuard mesh (Tailscale) gives the phone a direct, encrypted path to the home PC with no ports opened to the internet. That encryption is load-bearing: the board and API speak plain
http://, so WireGuard is the only thing protecting the traffic in flight.
The fuller tradeoff (Tailscale versus Cloudflare Tunnel versus port-forwarding) is a decision record in its own right and is out of scope here; this guide sticks to getting the mesh working.
Prerequisites
- A Tailscale account (the free tier is plenty). You can sign in with Google, Microsoft, GitHub, Apple, or a passkey; no separate password to manage.
- The ability to run the API on the home PC, bound to the mesh interface (
0.0.0.0) rather than justlocalhost. Step 3 is the exact command; Step 5 makes it survive reboots. - A client with a reachable API URL. The web board self-connects to whatever origin served it, so opening
http://100.x.y.z:8000in a browser needs no configuration. The mobile app has an editable API URL field whose default is a LAN address (http://192.168.1.20:8000), which is exactly the value you will replace with the mesh address.
Step 1: Put the host PC on the tailnet
On the home PC, install Tailscale, bring it up, and read back its mesh address:
winget install Tailscale.Tailscale
tailscale up # opens a browser to log in
tailscale ip -4 # the PC's mesh address, e.g. 100.x.y.z
tailscale up opens a browser once to authenticate the machine. tailscale ip -4 prints a single address in the 100.64.0.0/10 range (Tailscale’s slice of CGNAT space), for example 100.101.102.103. That address is stable for this machine, so you can note it once and reuse it.
Step 2: Put the phone on the same tailnet
- Install the Tailscale app (App Store / Google Play) and log in with the same account you used on the PC.
- Toggle it on. The phone can now reach the PC at its
100.x.y.zaddress from anywhere, on any network.
Step 3: Start the API on the mesh interface
On the home PC, start the API bound to 0.0.0.0 so it listens on the tailnet and not just localhost:
cd api
uvicorn app.main:app --host 0.0.0.0 --port 8000
The default dev command (uvicorn app.main:app --reload) binds 127.0.0.1 and will not answer over the tailnet, so this is the one detail that silently breaks everything downstream if you skip it. To make the API come up on its own, windowless, at every logon, see Step 5.
Serve a local board build, not the cloud one. The API serves whatever is in
desktop/distat/. Build it withnpm run build --workspace desktopand noVITE_*cloud variables set, so the bundle self-connects to whatever origin serves it. A cloud build is hard-pinned to the AWS API and a login screen, so it will not talk to your home API over the tailnet: the board loads in the browser but every request fails with an API error. If you have ever runinfra/deploy_spa.sh, it left a cloud build indist; rebuild locally before serving.
Step 4: Point the client at the mesh address
- Browser: open
http://100.x.y.z:8000. The API serves the board, and the board self-connects back to that same origin, so there is nothing else to set. - Mobile app: open the API URL field and replace the default with
http://100.x.y.z:8000. The value is saved for next time.
Then move a task, and a receipt prints at home.
Step 5: Keep it reachable (optional but recommended)
Two things keep the board available whenever you reach for your phone, rather than only when you happen to be sitting at the PC.
- Auto-start the API so it is always running. The repo’s
api/serve-hidden.vbslaunches uvicorn windowless, bound to0.0.0.0, logging toapi/api.log; point a shortcut at it from your Windows Startup folder and the API comes up at logon with no window and no admin rights. Tailscale itself already runs as a service on boot, so it needs no equivalent step. - Stop the PC from sleeping, or it drops off the tailnet and the phone gets a connection error. Set the PC’s power plan so it stays awake (or wake it on demand) for the hours you expect to use the board.
Verify
A concrete, physical success test:
- Phone on cellular, Wi-Fi off, Tailscale on.
- Open
http://100.x.y.z:8000and confirm the board loads. - Drag or tap a task into a new swimlane, and confirm the RP850 prints the receipt at home.
If the board does not load, confirm the API is running bound to 0.0.0.0, not 127.0.0.1 (see Step 3).
If all three hold, you are reaching an unauthenticated home service from the open internet with nothing exposed to it.
Caveats
- There is no authentication here: this is safe only because the mesh is private. This build has no login to enable, so never point it at a public URL; a build that needs public exposure needs an auth layer added first.
- Plain HTTP, encrypted by the mesh: the board and API speak
http://with no TLS, so WireGuard is the only thing encrypting the traffic. That is fine inside the tailnet and only inside it; never expose this origin publicly. - The tailnet is your trust boundary: with no auth in front of the API, anyone on your tailnet can reach it. Adding a device to the tailnet effectively grants board access, so keep the tailnet small and private, and reach for Tailscale ACLs if you share it.
- The host must be awake with the API running; a sleeping PC is unreachable.
- No firewall changes are needed: Tailscale permits its own traffic, so you do not have to open a Windows firewall port for
8000. That is a side effect of the mesh, not something you configure.
See also
- TaskBoard project write-up: the whole system and why it is shaped this way.
- TaskBoard Printer Integration: how moving a task between swimlanes turns into a receipt on the RP850.