Skip to content
About

Ripped the portal off its rented server and bolted it to my desk

Migrated honest-cam's state to SQLite and self-hosted the portal on a Mac mini via Cloudflare Tunnel.

4 min readEvergreen8:01amMiami Beach, FL, USA

Today was moving day. The honest-cam portal has been living on a rented Linux box, and Phase 3 is about getting it onto hardware I actually own — a Mac mini on my desk. I want to run local RAG against the same data the portal reads, and that only works if the model and data sit on the same machine. Two PRs, one goal: yank the portal off its landlord server and bolt it to my desk.

First, the packing (PR #16)

You can't move a house if the furniture is glued to the floor. The portal's mutable state — compliance events, study progress — was stored in plain JSON files pinned to one filesystem. If the server hiccupped mid-write, my recovery plan was basically "hope fcntl did its job." Not great.

So I swapped the sticky notes for a real database. A new db/ module gives the portal two small SQLite databases:

  • Per-property (data/{slug}/honestcam.db): compliance events for that property. Append-only — no updates, no deletes.
  • Global (honestcam-study.db): study users and progress across all properties. Last write wins, same as the old JSON files, just sturdier.

Both run in WAL mode. That means the portal can serve reads while a write is happening, and if the power blips mid-write you get a recoverable journal instead of a half-written file. Load-bearing improvement right there.

The migration itself was satisfying. Two CLI commands: honestcam db-migrate bamboo-house --dry-run to peek, then --execute to do it. Legacy JSON gets archived to _legacy/{timestamp}/. Running --execute a second time is a no-op — if you can't safely run a migration twice, it's not a migration, it's a prayer.

I also did the rollback drill: moved the new .db aside, restored the JSON, hit the portal. Everything came back. The old JSON reader was never removed, so the backout plan is move the DB, drop the JSON back, restart. Done.

The public surface didn't change at all. compliance_routes.py needed zero edits. The apply_events engine? Untouched. That's the nice part of keeping your layers clean — swapping the storage underneath didn't ripple up.

Then, the actual move (PR #17)

With portable state in hand, time to point the portal at its new home. The topology is neat:

https://honestcam.ocampo.io hits the Cloudflare edge, which follows an outbound-initiated tunnel down to the Mac mini, which runs the portal. The key detail: the Mac mini dials out to Cloudflare. Cloudflare does not dial in. My home router has zero inbound holes punched. Nothing on the public internet can reach the mini unless the mini invited it first.

Cloudflare Access sits in front of the tunnel. A request gets 302'd to a login page, and only allowlisted emails get through. The portal doesn't know anything about identity — it trusts that any request at its socket is already authenticated, because the only path to that socket goes through the tunnel, and the only path through the tunnel goes past Access.

Two launchd user agents keep everything alive: one for cloudflared tunnel run, one for uvicorn. Both set RunAtLoad=true with KeepAlive. Crash? launchd brings it back. Reboot? Both come up with the user session.

Along the way I found that website-populate was emitting absolute symlinks, which all broke the moment I moved the data directory. Switched to relative and rebuilt 952 broken links in place. The kind of bug that hides until moving day.

Proof it works

  • 660/660 tests pass across both PRs. No regressions.
  • curl -I https://honestcam.ocampo.io/ returns a 302 to Cloudflare Access. Login flow verified from a cold browser.
  • Compliance POST landed a row in SQLite and survived a restart.
  • Migration ran idempotently. Rollback round-tripped cleanly.

Still on my desk: pull the power cord and confirm RunAtLoad actually does what it claims after a hard reboot, and walk through the macOS energy settings so the mini doesn't decide to nap at an inconvenient moment.

The portal is home. Next up: Litestream replicating the SQLite WAL to Cloudflare R2 so a dead Mac mini doesn't take the state with it, and then the actual interesting part — pointing local RAG at the data sitting right there on the same box.


PRs:

Related posts

Stay in the loop

Newsletter launching soon — follow along via RSS.