---
title: "Single-origin deployment with bhvr"
section: posts
description: "Fried my Pi using a 10V solar panel without a buck converter."
tags: ["raspberry-pi", "hardware", "solar-powered", "web-development", "cloudflare-tunnel"]
created: 2025-05-13T17:34:00Z
updated: 2025-05-23T14:09:00Z
cid: bafkreibf6c3xwvhiszc2y6hwzxaxq7qizamhgw4n22jhzlfnz4qe2dvp3i
html: https://iammatthias.com/posts/1747182886660-single-origin-deployment-with-bhvr
---

# Single-origin deployment with bhvr

I fried a Raspberry Pi Zero 2 W. A 10-volt solar panel running straight into the [Pi's 5 V USB](https://www.amazon.com/dp/B07XZ8BD6J) 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:

```ts
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](https://bun.sh/) became the runtime and package manager. [bhvr](https://bhvr.dev/) 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

```bash
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](http://localhost:3000/) and [localhost:3000/api/hello](http://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.

```bash
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

```ini
# /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
```

```bash
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

```yaml
# /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
```

```bash
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

```bash
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.

---

## Related

- [Pure Internet](https://iammatthias.com/posts/1731955749292-pure-internet.md): Pure Internet is a running collection of small experiments in publishing with fewer layers.
- [Pure Internet: Feral](https://iammatthias.com/posts/1733357455673-pure-internet-feral.md): A solar-powered Raspberry Pi runs a small Hono app through a Cloudflare Tunnel and disappears when the battery drains.
- [Now Playing](https://iammatthias.com/posts/1784736733544-now-playing.md): Putting my Apple Music "Now Playing" on display with a Raspberry Pi and a 64x64 LED matrix.
- [Intern](https://iammatthias.com/posts/1781835006151-intern.md): The Autonomous Intern ships as a Raspberry Pi 5 in a desk-toy case running a stack I didn't pick. I flashed it to Debian Trixie, swapped the whole thing for the Hermes agent with its own built-in memory, and folded it all into one setup script.
- [Adventures in Machine Vision](https://iammatthias.com/posts/1780360240000-adventures-in-machine-vision.md): A Nat Friedman anecdote about an AI watching him drink water sent me digging for a spare Raspberry Pi. Now a camera and a sensor HAT watch my desk, a self-inflicted panopticon that logs who walks by and what the room is doing.
