{"collection":"posts","slug":"1747182886660-single-origin-deployment-with-bhvr","cid":"bafkreibf6c3xwvhiszc2y6hwzxaxq7qizamhgw4n22jhzlfnz4qe2dvp3i","title":"Single-origin deployment with bhvr","excerpt":"Fried my Pi using a 10V solar panel without a buck converter.","body":"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.\n\nWhile I waited on a new board and a buck converter, I rebuilt the site from scratch.\n\n## Old stack\n\n```\nNode.js (ARMv6 build)  \nPNPM  \nHono + hono/jsx  \nCloudflare Tunnel\n````\n\nEverything lived in one server file using `hono/jsx`. HTML, styles, scripts, and API data all came out of the same Hono route handlers:\n\n```ts\napp.get(\"/\", (c) =\u003e c.html(`\u003ch1\u003eAir Quality\u003c/h1\u003e`));\n```\n\nIt 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.\n\nMarkdown 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.\n\n## Rebuild\n\nI 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.\n\n## New stack\n\n```\nbhvr (Bun + Hono + Vite + React)  \nCloudflare Tunnel\n```\n\nThree workspaces:\n\n```\nclient/   → React + Vite frontend  \nserver/   → Hono API + static server  \nshared/   → TypeScript types used by both\n```\n\nThe 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.\n\n## Rebuilding locally\n\n```bash\ngit clone https://github.com/iammatthias/feral-pure-internet.git\ncd feral-pure-internet\nbun install\n\nbun run build \u0026\u0026 bun run build:server\nmkdir -p server/dist/client\ncp -r client/dist/* server/dist/client/\n\nbun run server/dist/server/src/index.js\n```\n\nA 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).\n\n## Pi deployment\n\nThis time, a proper 5 V buck converter stepped the solar panel's 10 V output down. No smoke.\n\n```bash\ncurl -fsSL https://bun.sh/install | bash\necho 'export PATH=\"$HOME/.bun/bin:$PATH\"' \u003e\u003e ~/.bashrc\nsource ~/.bashrc\n```\n\nClone to `/opt/feral/app`, run `bun install`, and build as above. Wrap it in `build.sh` for automation.\n\n## Systemd\n\n```ini\n# /etc/systemd/system/feral.service\n[Unit]\nDescription=Feral, single-origin Bun server\nAfter=network-online.target\n\n[Service]\nUser=feral\nWorkingDirectory=/opt/feral/app\nEnvironment=WAQI_TOKEN=•••\nExecStart=/home/feral/.bun/bin/bun run server/dist/server/src/index.js\nRestart=always\nRestartSec=5\n\n[Install]\nWantedBy=multi-user.target\n```\n\n```bash\nsudo systemctl daemon-reload\nsudo systemctl enable --now feral\n```\n\nThis replaces PM2. It restarts on crash or reboot without \"save\" or \"startup\" steps.\n\n## Tunnel on a single port\n\n```yaml\n# /etc/cloudflared/config.yml\ntunnel: \u003cUUID\u003e\ncredentials-file: /etc/cloudflared/\u003cUUID\u003e.json\n\ningress:\n  - hostname: feral.pure---internet.com\n    service: http://localhost:3000\n  - service: http_status:404\n```\n\n```bash\ncloudflared tunnel create feral\ncloudflared tunnel route dns feral feral.pure---internet.com\nsudo systemctl enable --now cloudflared\n```\n\nUI and API both live at `https://feral.pure---internet.com`. No ports, no CORS, no dev/prod shims.\n\n## Update flow\n\n```bash\ncd /opt/feral/app\ngit pull --ff-only\nbun run build \u0026\u0026 bun run build:server\nmkdir -p server/dist/client\ncp -r client/dist/* server/dist/client/\nsudo systemctl restart feral\n```\n\nA post-merge hook or cron job will automate it. Takes under 5 seconds.\n\n## Before and after\n\n||Old|New|\n|---|---|---|\n|Runtime|Node v20 (ARMv6 build)|Bun|\n|Package mgr|PNPM|None (Bun native)|\n|Front-end|hono/jsx|React + Vite (via bhvr)|\n|Dev build|Rollup + tsc|Bun + Vite|\n|Deployment|PM2|systemd|\n|Architecture|Single file, no separation|client/server/shared|\n|Serving style|hono SSR + inline JSX|Static bundle + JSON API|\n|Origin setup|Single-origin|Single-origin|\n\nThe 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 `\u003cdiv\u003e`.\n\nSingle-origin deployments suit bhvr well, especially when the whole stack fits in a few megabytes on a Pi Zero 2 W.","tags":["raspberry-pi","hardware","solar-powered","web-development","cloudflare-tunnel"],"published":true,"createdAt":"2025-05-13T17:34:00Z","updatedAt":"2025-05-23T14:09:00Z"}
