Single-origin deployment with bhvr
3 min read
Fried my Pi using a 10V solar panel without a buck converter.
On this page
I fried a Raspberry Pi Zero 2 W. A 10-volt solar panel running straight into the Pi's 5 V USB was not the move.
While I waited on a new board and a buck converter, I rebuilt the site from scratch.
Old stack
Node.js (ARMv6 build)
PNPM
Hono + hono/jsx
Cloudflare Tunnel
Everything lived in one server file using hono/jsx. HTML, styles, scripts, and API data all came out of the same Hono route handlers:
app.get("/", (c) => c.html(`<h1>Air Quality</h1>`));
It worked, but the file was cramped: no hot reload without a full restart, no separation between UI and API, boilerplate for the smallest content tweak.
Markdown rendered server-side. The live-data API (air quality, device location) ran on the same routes serving HTML. Everything came out of port 3000 behind a Cloudflare Tunnel. When the Pi slept, the site slept. That was the point.
Rebuild
I dropped Node and PNPM. Bun became the runtime and package manager. bhvr gave me a monorepo layout for Bun + Hono + React + Vite. The single-origin tunnel stayed: one server, one port, one pipe.
New stack
bhvr (Bun + Hono + Vite + React)
Cloudflare Tunnel
Three workspaces:
client/ → React + Vite frontend
server/ → Hono API + static server
shared/ → TypeScript types used by both
The React app builds separately, and its static files drop into server/dist/client. One Bun process on port 3000 serves both the frontend and the API.
Rebuilding locally
git clone https://github.com/iammatthias/feral-pure-internet.git
cd feral-pure-internet
bun install
bun run build && bun run build:server
mkdir -p server/dist/client
cp -r client/dist/* server/dist/client/
bun run server/dist/server/src/index.js
A static React site and Hono API are now live at localhost:3000 and localhost:3000/api/hello.
Pi deployment
This time, a proper 5 V buck converter stepped the solar panel's 10 V output down. No smoke.
curl -fsSL https://bun.sh/install | bash
echo 'export PATH="$HOME/.bun/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
Clone to /opt/feral/app, run bun install, and build as above. Wrap it in build.sh for automation.
Systemd
# /etc/systemd/system/feral.service
[Unit]
Description=Feral, single-origin Bun server
After=network-online.target
[Service]
User=feral
WorkingDirectory=/opt/feral/app
Environment=WAQI_TOKEN=•••
ExecStart=/home/feral/.bun/bin/bun run server/dist/server/src/index.js
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now feral
This replaces PM2. It restarts on crash or reboot without "save" or "startup" steps.
Tunnel on a single port
# /etc/cloudflared/config.yml
tunnel: <UUID>
credentials-file: /etc/cloudflared/<UUID>.json
ingress:
- hostname: feral.pure---internet.com
service: http://localhost:3000
- service: http_status:404
cloudflared tunnel create feral
cloudflared tunnel route dns feral feral.pure---internet.com
sudo systemctl enable --now cloudflared
UI and API both live at https://feral.pure---internet.com. No ports, no CORS, no dev/prod shims.
Update flow
cd /opt/feral/app
git pull --ff-only
bun run build && bun run build:server
mkdir -p server/dist/client
cp -r client/dist/* server/dist/client/
sudo systemctl restart feral
A post-merge hook or cron job will automate it. Takes under 5 seconds.
Before and after
| Old | New | |
|---|---|---|
| Runtime | Node v20 (ARMv6 build) | Bun |
| Package mgr | PNPM | None (Bun native) |
| Front-end | hono/jsx | React + Vite (via bhvr) |
| Dev build | Rollup + tsc | Bun + Vite |
| Deployment | PM2 | systemd |
| Architecture | Single file, no separation | client/server/shared |
| Serving style | hono SSR + inline JSX | Static bundle + JSON API |
| Origin setup | Single-origin | Single-origin |
The app is tiny, ephemeral, and still solar-powered. Rebuilding with bhvr added real structure: client and server actually separated, deploys and updates much simpler, lighter runtime (just Bun and a couple of systemd units), and no more crashing when I tweak a <div>.
Single-origin deployments suit bhvr well, especially when the whole stack fits in a few megabytes on a Pi Zero 2 W.