Exposing this beyond localhost
Run with python3 -m backend.main or make dev and the app only answers requests from the
machine it’s running on (defaults to binding 127.0.0.1). make serve — the project’s own
without---reload run command — binds every network interface instead (0.0.0.0), reachable
from any other device on your LAN the moment it starts, with no separate “expose” step.
make daemon-start is the same story: it loads a launchd service
(~/Library/LaunchAgents/ai.opennetworth.backend.plist) that runs that exact LAN-bound command —
the Makefile’s own comment on the serve target says so directly. This plist is your own
machine’s local state, not something this repo ships or generates, so if you wrote it yourself
with a different invocation, its actual bind host is whatever you put in it — but the project’s
own convention for it is LAN-bound, same as make serve, not 127.0.0.1. Read this page either
way: this app holds your complete financial picture behind a single password, and reachability
beyond your own machine changes what protects it.
What protects you today, and what doesn’t
Out of the box, the app is protected by exactly one thing: your instance password. No built-in encryption in transit, no second factor, no per-device approval — anyone who has (or guesses) that password, and can reach the port the app is listening on, is in.
- The app itself doesn’t speak HTTPS. It’s a plain HTTP server. Expose it directly — port- forwarded on your router, or bound to a public interface with nothing in front of it — and your password and session cookie travel over the network in plain text, readable by anything positioned to see that traffic.
- Login lockout resets on restart. Five wrong attempts from the same address locks further attempts out for 15 minutes, but that counter lives in memory — restarting the backend clears it.
- There’s no IP allowlist or VPN built in. Reachability is entirely about what network the app’s port is exposed to; the app itself doesn’t restrict who can attempt to log in from wherever it’s reachable.
None of this is a bug — it’s the honest state of a self-hosted, single-operator app that assumes you’re the one deciding how it’s reached.
What to put in front of it before you expose it
Put a reverse proxy with real TLS in front of the app, or reach it over a VPN/tunnel — don’t bind it directly to a public interface. One of:
- A reverse proxy (Caddy, nginx, Traefik) terminating HTTPS with a real certificate, forwarding to the app locally.
- A private tunnel (Tailscale, WireGuard, or similar), so the app is only ever reachable over a network you control, never the open internet.
Either way, your password and session cookie are encrypted in transit and the app never directly faces an untrusted network.
The one setting a same-machine reverse proxy makes critical
By default, the app trusts any request reaching it from the machine it’s running on — the convenience that skips login for local use. A reverse proxy you run on that same machine connects to the backend from that same machine, so — unless you turn this off — every request it forwards, including ones that started on the public internet a moment earlier, is trusted exactly as if you’d typed the URL on the server’s own keyboard. No password is asked for at all. This is the single most important setting on this page: a reverse proxy is not, by itself, a login screen.
Before putting any reverse proxy in front of this app on the same host, turn this trust off (set
AUTH_TRUST_LOOPBACK=false and restart) so the app checks a real password/session for every
forwarded request, exactly like any remote client. Leave it on only if the app truly never
receives traffic from anywhere but your own logged-in shell on that box — a private tunnel
(Tailscale, WireGuard) terminating as a normal network interface has the same consideration:
check where the connection the app actually sees comes from, not just what reaches your router.
If you loosen CORS
The app’s cross-origin policy defaults to two local development addresses — nothing else can make
browser-based requests to it out of the box. Serving the frontend from a different origin than
the backend (a real production layout) means widening this to your own domain. Avoid opening it
to every origin (*) — a session cookie isn’t sent on a cross-origin request under that setting
anyway, so opening it wide buys nothing while loosening a boundary that costs nothing to leave
narrow.
The honest summary
Treat the instance password the way you’d treat the one credential standing between the internet and your bank statements — because that’s what it is here. TLS via a reverse proxy or a private network is not optional the moment this leaves your own machine; it’s the one piece the app deliberately leaves to you.
Related pages
operations/security-and-privacy— what’s protected already, and what data existsoperations/multi-device— reaching your instance from more than one device, once exposed safely