Backend over Cloudflare Tunnel¶
The mobile app needs a backend it can reach from anywhere: TestFlight testers are not on your Wi-Fi, so a LAN address will not do. A Cloudflare Tunnel gives the backend running on your laptop a public HTTPS hostname, with no ports opened on your router and no cloud hosting bill.
That hostname is what goes into EXPO_PUBLIC_API_URL for preview and
production builds.
Use a named tunnel, not a quick tunnel¶
cloudflared tunnel --url http://localhost:8000 works with no account and
prints a *.trycloudflare.com URL. It is useful for a one-off check, but it
is the wrong tool here: the URL is regenerated every time cloudflared
restarts.
Expo inlines EXPO_PUBLIC_API_URL into the JavaScript bundle at build time,
so a build carrying a quick-tunnel URL stops working the moment you restart
the tunnel, and fixing it means a new build and a new TestFlight release.
A named tunnel bound to a hostname you control survives restarts, reboots, and IP changes. Use that.
Requirements¶
- A Cloudflare account.
- A domain in that account (the tunnel routes a subdomain of it).
cloudflaredinstalled:brew install cloudflared.
One-time setup¶
# 1. Authorize cloudflared and pick your domain in the browser window.
# Writes ~/.cloudflared/cert.pem.
cloudflared tunnel login
# 2. Create the tunnel. Prints a UUID and writes
# ~/.cloudflared/<UUID>.json, which is the tunnel's credential.
cloudflared tunnel create senior-health-api
# 3. Point a hostname at it. This creates the DNS record for you.
cloudflared tunnel route dns senior-health-api api.example.com
Then write ~/.cloudflared/config.yml, using
backend/tunnel.example.yml
as the template:
tunnel: senior-health-api
credentials-file: /Users/YOUR_USER/.cloudflared/YOUR_TUNNEL_UUID.json
ingress:
- hostname: api.example.com
service: http://localhost:8000
- service: http_status:404
The trailing http_status:404 catch-all is required; cloudflared refuses to
start without one.
Running it¶
Or separately, which is easier to debug:
Verify from a machine that is not yours (or from your phone on cellular, with Wi-Fi off, which is the case that actually matters):
Pointing the app at it¶
cd apps/mobile
eas env:create --environment production \
--name EXPO_PUBLIC_API_URL --value https://api.example.com --visibility plaintext
No trailing path: the app appends /health and /transcribe itself. Trailing
slashes are stripped, so https://api.example.com/ is also fine.
CORS_ORIGINS does not need changing for TestFlight. CORS is a browser rule
and native iOS ignores it; it only matters for the Expo web build.
DEEPGRAM_API_KEY does need to be set wherever the backend runs, or
/transcribe answers 502 and voice answering stays broken. It is read from
backend/.env in local development.
What this costs you¶
The laptop is the server. That means:
- It must be awake, online, and running both processes whenever a tester opens the app. A closed lid means voice answers fail. Everything else in the app keeps working, because music previews, video, and photos are fetched directly or bundled.
- Cloudflare Tunnel itself is free on any plan, including Free.
For a handful of known testers this is a reasonable trade. For anything
longer-lived, deploy the container instead: backend/Dockerfile is
self-contained and runs on any container host.
Security¶
The backend has no authentication. Once tunneled, anyone who learns the hostname can reach every endpoint, which means they can spend your Deepgram credits and run ffmpeg on your laptop.
Mitigations worth applying:
- Keep the hostname unguessable, and do not publish it.
- Run the tunnel only during test sessions rather than leaving it up.
MAX_UPLOAD_BYTEScaps upload size (default 50 MB); lower it if you like.- Add a Cloudflare WAF rate-limiting rule on the hostname.
Cloudflare Access is the usual answer for locking a tunnel down, but it expects a browser login or a service token, and the app sends neither, so turning it on would block the app as well.