The long-term goal of the second-brain project is to run local RAG against my own notes on hardware I own, so the model and the data sit on the same box. honest-cam is the sandbox I'm using to shake out the ops story first, and Phase 3c is the step where the portal actually starts serving production traffic from the Mac mini on my desk instead of a rented Linux host.
Tier 1 of Phase 3c is "get the public URL live and keep it alive through crashes and reboots." This PR is that tier.
The topology
https://honestcam.ocampo.io → Cloudflare edge → outbound-initiated tunnel → Mac mini → portal. The important property is the tunnel direction: the Mac mini dials out to Cloudflare; Cloudflare does not dial in. My home router has zero inbound holes punched for this. Nothing on the public internet can reach the mini unless the mini invited it first, and the only invitation is the tunnel.
Cloudflare Access sits in front of the tunnel on the edge side. A request for honestcam.ocampo.io hits Cloudflare, gets 302'd to a login page, and only if the requesting email is on the allowlist does Cloudflare proxy the request down the tunnel to the portal. The portal itself doesn't know anything about identity — it trusts that any request arriving at its uvicorn socket is already authenticated, because by construction the only way to reach that socket is through the tunnel, and the only way through the tunnel is after Access.
Keeping it alive
Two launchd user agents, both RunAtLoad=true with KeepAlive, one for cloudflared and one for the portal:
~/Library/LaunchAgents/com.honestcam.cloudflared.plist— runscloudflared tunnel run, pointed at~/.cloudflared/config.yml.~/Library/LaunchAgents/com.honestcam.portal.plist— runs uvicorn againsthonestcam.portal.app:create_app, withHONESTCAM_DATA_DIRandPORTAL_PROPERTY_SLUGbaked into the plist environment.
launchctl list | grep honestcam shows both. If either process crashes, launchd brings it back; if the Mac reboots, both come up with the user session. The actual plist contents, the cloudflared config, and my ~/.zshrc additions live outside the repo (they're host-specific), but docs/feature-self-hosting.md captures them verbatim so future-me can rebuild from scratch.
Collateral hardening
Two changes I made along the way because the migration uncovered them:
-
Fail fast on missing
PORTAL_PROPERTY_SLUG.create_app()now raises at startup if the env var is unset, instead of letting requests through and blowing up with a mysterious 500 at the first query that needs a property context. Any boot path — launchd, direct uvicorn, a test harness — surfaces the same clear error. -
Relative symlinks for
website-populate. The shared-docs tree is materialized as a pile of symlinks into the per-property docs directory. It was previously emitting absolute paths, which meant every one of those symlinks broke the moment I moved the data directory. I changedwebsite-populateto emit relative symlinks and also to rewrite any existing absolute ones in place. 952 broken links rebuilt, verified withreadlink. The shared-docs tree now survives data-dir moves.
Verification
pytest— 660/660 pass (no regressions from #16).ruff check .— clean.launchctl list | grep honestcam— both agents running.curl -I https://honestcam.ocampo.io/→ 302 → Cloudflare Access. Email login flow verified end to end from a cold browser session.- 952 shared-docs symlinks rebuilt as relative, spot-checked with
readlink.
Deferred, with a TODO on my desk:
- Pull the power cord and confirm
RunAtLoad=trueactually does what it claims after a hard reboot. - Walk through the macOS System Settings GUI checklist (Energy Saver → wake for network access, Firewall, Software Update behavior) so the mini doesn't nap or auto-reboot at an inconvenient moment.
Where this leaves Phase 3c
Tier 1 is the box and the address. Tier 2 is Litestream continuously replicating the SQLite WAL to Cloudflare R2 so a dead Mac mini doesn't take the portal's state with it. Tier 3 is pointing local RAG at ~/honestcam-production-data/ and closing the loop on the second-brain goal — running a model on the same machine as the data it's reading. That's the actual interesting work. This PR was the plumbing that has to exist before any of it is safe to turn on.