I recently fried a Raspberry Pi Zero 2 W. Turns out a p10‑volt solar panel running straight into the Pi’s 5 V USB isn’t the best decision.
While I waited for a new board (and a buck converter) to arrive, I revisited the site that little SBC was hosting—and used the downtime to rebuild it from the ground up.
The Original Stack
Node.js (ARMv6 build)
PNPM
Hono + hono/jsx
Cloudflare Tunnel
Everything lived in a single server file using hono/jsx
. HTML, styles, scripts, and API data were assembled and returned from Hono route handlers:
app.get("/", (c) => c.html(`<h1>Air Quality</h1>`));
This worked, but it was cramped:
- No hot reload unless I restarted the whole server
- No separation between UI and API logic
- Lots of boilerplate for even simple content updates
Markdown files rendered on the server. The API for live data (air quality, device location) was mixed into the same routes that served HTML. Everything was served from port 3000 behind a Cloudflare Tunnel, and when the Pi went to sleep, so did the site. That was the point.
The Rebuild
While the hardware was in the mail, I decided to rebuild the project from scratch:
- Drop Node and PNPM entirely
- Use Bun as both runtime and package manager
- Adopt bhvr, a monorepo stack for Bun + Hono + React + Vite
- Keep the single-origin tunnel: one server, one port, one pipe
The New Stack
bhvr (Bun + Hono + Vite + React)
Cloudflare Tunnel
Now the project is split into three clear workspaces:
client/ → React + Vite frontend
server/ → Hono API + static server
shared/ → TypeScript types used by both
Instead of rendering HTML on the server, I build the React app separately and drop the compiled static files into server/dist/client
. Then I serve both the frontend and API from a single Bun process on port 3000.
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
You now get a fully functional static React site + Hono API available at:
Pi Deployment
This time, I used a proper 5 V buck converter to step down the solar panel’s 10 V output. (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 like above. You can wrap the steps in a build.sh
script for automation.
Systemd Setup
# /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 the old PM2 setup. It’s faster, cleaner, and restarts on crash or reboot without needing to “save” or “startup” anything.
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
With this setup, everything—the UI, the API—is available 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
Add a post-merge hook or cron job if you want to 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 |
Final Thoughts
The app is still tiny. Still ephemeral. Still solar-powered. But rebuilding it with bhvr gave me real structure:
- Clear separation between client and server
- Faster deploys and simpler updates
- Lighter runtime—just Bun and a few systemd units
- No more crashing when I tweak a
<div>
Single-origin deployments are a great match for bhvr—especially when the whole stack lives inside a few megabytes on a Pi Zero 2 W.