Merge pull request #2 from iammatthias/main
0b1c514d
1 file(s) · +295 −59
| 4 | 4 | ||
| 5 | 5 | Serve both your frontend and API from the same process, same port, and same origin—ideal for fullstack apps where simplicity matters. |
|
| 6 | 6 | ||
| 7 | - | This guide walks through deploying a bhvr project using a **single origin** approach. You’ll still get React + Vite on the frontend, Hono on the backend, and Bun running it all. |
|
| 7 | + | This guide walks through configuring your bhvr project for **single origin** deployment, where your React frontend and Hono API run from the same Bun process. |
|
| 8 | 8 | ||
| 9 | 9 | Perfect for: |
|
| 10 | 10 | ||
| 11 | 11 | - VPS deployments |
|
| 12 | 12 | - Raspberry Pis |
|
| 13 | 13 | - Home servers |
|
| 14 | + | - Docker containers |
|
| 14 | 15 | - Projects where you want one URL to rule them all |
|
| 15 | 16 | ||
| 16 | - | ## What Is a Single Origin Deployment? |
|
| 17 | + | ## Prerequisites |
|
| 18 | + | ||
| 19 | + | This guide assumes you have a bhvr project set up. If not, start here: |
|
| 20 | + | ||
| 21 | + | ```bash |
|
| 22 | + | bun create bhvr@latest my-app |
|
| 23 | + | cd my-app |
|
| 24 | + | ``` |
|
| 25 | + | ||
| 26 | + | <Button href='/getting-started'>Getting Started with bhvr</Button> |
|
| 27 | + | ||
| 28 | + | ## What Is Single Origin? |
|
| 29 | + | ||
| 30 | + | Instead of running your client and server separately (the default bhvr setup), single origin serves everything from one process: |
|
| 31 | + | ||
| 32 | + | **Default bhvr setup:** |
|
| 33 | + | ||
| 34 | + | - Client runs on port 5173 (Vite dev server) |
|
| 35 | + | - Server runs on port 3000 (Hono API) |
|
| 36 | + | - Requires CORS for communication |
|
| 37 | + | ||
| 38 | + | **Single origin setup:** |
|
| 39 | + | ||
| 40 | + | - Everything runs on port 3000 |
|
| 41 | + | - Hono serves both API routes and static React files |
|
| 42 | + | - No CORS needed |
|
| 43 | + | ||
| 44 | + | ## Configuration |
|
| 45 | + | ||
| 46 | + | ### 1. Update Your Hono Server |
|
| 47 | + | ||
| 48 | + | Modify `server/src/index.ts` to serve static files alongside your API: |
|
| 49 | + | ||
| 50 | + | ```typescript |
|
| 51 | + | import { Hono } from "hono"; |
|
| 52 | + | import { cors } from "hono/cors"; |
|
| 53 | + | import { serveStatic } from "hono/bun"; |
|
| 54 | + | import type { ApiResponse } from "shared/dist"; |
|
| 55 | + | ||
| 56 | + | const app = new Hono(); |
|
| 57 | + | ||
| 58 | + | // CORS is optional for single origin deployment |
|
| 59 | + | // Keep for development flexibility, remove for production if desired |
|
| 60 | + | app.use(cors()); |
|
| 17 | 61 | ||
| 18 | - | A single origin setup serves your client and your API from the same runtime and port: |
|
| 62 | + | // Your existing API routes - keep the /api prefix for clarity |
|
| 63 | + | app.get("/api", (c) => { |
|
| 64 | + | return c.text("Hello Hono!"); |
|
| 65 | + | }); |
|
| 19 | 66 | ||
| 20 | - | - `bun` runs both your backend and static frontend |
|
| 21 | - | - `hono` handles both routes and static files |
|
| 22 | - | - Everything lives behind one port and one domain (like `https://yourapp.com`) |
|
| 67 | + | app.get("/api/hello", async (c) => { |
|
| 68 | + | const data: ApiResponse = { |
|
| 69 | + | message: "Hello BHVR!", |
|
| 70 | + | success: true, |
|
| 71 | + | }; |
|
| 72 | + | return c.json(data, { status: 200 }); |
|
| 73 | + | }); |
|
| 23 | 74 | ||
| 24 | - | No CORS. No extra hops. No need for a separate web server unless you prefer one. |
|
| 75 | + | // Add more API routes here with /api prefix |
|
| 76 | + | // app.get('/api/users', ...) |
|
| 77 | + | // app.post('/api/data', ...) |
|
| 25 | 78 | ||
| 26 | - | ## Folder Structure |
|
| 79 | + | // Serve static files for everything else |
|
| 80 | + | app.use("*", serveStatic({ root: "./static" })); |
|
| 27 | 81 | ||
| 28 | - | Single origin works great with bhvr’s monorepo setup: |
|
| 82 | + | const port = parseInt(process.env.PORT || "3000"); |
|
| 83 | + | ||
| 84 | + | export default { |
|
| 85 | + | port, |
|
| 86 | + | fetch: app.fetch, |
|
| 87 | + | }; |
|
| 29 | 88 | ||
| 89 | + | console.log(`🦫 bhvr server running on port ${port}`); |
|
| 30 | 90 | ``` |
|
| 31 | - | client/ → React + Vite frontend |
|
| 32 | - | server/ → Hono API backend |
|
| 33 | - | shared/ → Shared types and utilities |
|
| 91 | + | ||
| 92 | + | ### 2. Update Your React Client |
|
| 93 | + | ||
| 94 | + | Modify `client/src/App.tsx` to use relative API paths: |
|
| 95 | + | ||
| 96 | + | ```typescript |
|
| 97 | + | import { useState } from "react"; |
|
| 98 | + | import beaver from "./assets/beaver.svg"; |
|
| 99 | + | import { ApiResponse } from "shared"; |
|
| 100 | + | import "./App.css"; |
|
| 101 | + | ||
| 102 | + | function App() { |
|
| 103 | + | const [data, setData] = useState<ApiResponse | undefined>(); |
|
| 104 | + | ||
| 105 | + | async function sendRequest() { |
|
| 106 | + | try { |
|
| 107 | + | // Use relative path - works in both dev and production |
|
| 108 | + | const req = await fetch("/api/hello"); |
|
| 109 | + | const res: ApiResponse = await req.json(); |
|
| 110 | + | setData(res); |
|
| 111 | + | } catch (error) { |
|
| 112 | + | console.log(error); |
|
| 113 | + | } |
|
| 114 | + | } |
|
| 115 | + | ||
| 116 | + | return ( |
|
| 117 | + | <> |
|
| 118 | + | <div> |
|
| 119 | + | <a href='https://github.com/stevedylandev/bhvr' target='_blank'> |
|
| 120 | + | <img src={beaver} className='logo' alt='beaver logo' /> |
|
| 121 | + | </a> |
|
| 122 | + | </div> |
|
| 123 | + | <h1>bhvr</h1> |
|
| 124 | + | <h2>Bun + Hono + Vite + React</h2> |
|
| 125 | + | <p>A typesafe fullstack monorepo</p> |
|
| 126 | + | <div className='card'> |
|
| 127 | + | <button onClick={sendRequest}>Call API</button> |
|
| 128 | + | {data && ( |
|
| 129 | + | <pre className='response'> |
|
| 130 | + | <code> |
|
| 131 | + | Message: {data.message} <br /> |
|
| 132 | + | Success: {data.success.toString()} |
|
| 133 | + | </code> |
|
| 134 | + | </pre> |
|
| 135 | + | )} |
|
| 136 | + | </div> |
|
| 137 | + | <p className='read-the-docs'>Click the beaver to learn more</p> |
|
| 138 | + | </> |
|
| 139 | + | ); |
|
| 140 | + | } |
|
| 141 | + | ||
| 142 | + | export default App; |
|
| 34 | 143 | ``` |
|
| 35 | 144 | ||
| 36 | - | After building, you copy the output from `client` into the `server` so it can be served statically. |
|
| 145 | + | ### 3. Configure Vite for Development |
|
| 37 | 146 | ||
| 38 | - | ## Why Use Single Origin? |
|
| 147 | + | Update `client/vite.config.ts` to proxy API calls during development: |
|
| 39 | 148 | ||
| 40 | - | - **No CORS headaches** |
|
| 41 | - | - **One tunnel or reverse proxy** to manage |
|
| 42 | - | - **Cleaner mental model** for small or embedded deployments |
|
| 43 | - | - Great fit for **low-resource devices** like Raspberry Pi |
|
| 149 | + | ```typescript |
|
| 150 | + | import { defineConfig } from "vite"; |
|
| 151 | + | import react from "@vitejs/plugin-react"; |
|
| 152 | + | import path from "path"; |
|
| 44 | 153 | ||
| 45 | - | ## Build and Launch |
|
| 154 | + | export default defineConfig({ |
|
| 155 | + | plugins: [react()], |
|
| 156 | + | resolve: { |
|
| 157 | + | alias: { |
|
| 158 | + | "@client": path.resolve(__dirname, "./src"), |
|
| 159 | + | "@server": path.resolve(__dirname, "../server/src"), |
|
| 160 | + | "@shared": path.resolve(__dirname, "../shared/src"), |
|
| 161 | + | }, |
|
| 162 | + | }, |
|
| 163 | + | server: { |
|
| 164 | + | proxy: { |
|
| 165 | + | "/api": { |
|
| 166 | + | target: "http://localhost:3000", |
|
| 167 | + | changeOrigin: true, |
|
| 168 | + | }, |
|
| 169 | + | }, |
|
| 170 | + | }, |
|
| 171 | + | }); |
|
| 172 | + | ``` |
|
| 46 | 173 | ||
| 47 | - | That’s it. Your entire app is now served on port 3000 by a single Bun process. |
|
| 174 | + | ### 4. Add Single Origin Scripts |
|
| 48 | 175 | ||
| 49 | - | ## Server Deployment Example |
|
| 176 | + | Add these scripts to your root `package.json` (alongside the existing bhvr scripts): |
|
| 50 | 177 | ||
| 51 | - | Whether you're deploying to a VPS, a Raspberry Pi, or a bare metal machine at home—if you can SSH into it and it runs a Unix-like OS, you're good to go. (This guide assumes a Linux/Unix server environment; Windows is not supported.) Here's a quick setup: |
|
| 178 | + | ```json |
|
| 179 | + | { |
|
| 180 | + | "scripts": { |
|
| 181 | + | "dev:client": "cd client && bun run dev", |
|
| 182 | + | "dev:server": "cd server && bun run dev", |
|
| 183 | + | "dev:shared": "cd shared && bun run dev", |
|
| 184 | + | "dev": "concurrently \"bun run dev:shared\" \"bun run dev:server\" \"bun run dev:client\"", |
|
| 185 | + | "build:client": "cd client && bun run build", |
|
| 186 | + | "build:shared": "cd shared && bun run build", |
|
| 187 | + | "build:server": "cd server && bun run build", |
|
| 188 | + | "build": "bun run build:shared && bun run build:client", |
|
| 189 | + | "build:single": "bun run build && bun run copy:static && bun run build:server", |
|
| 190 | + | "copy:static": "rm -rf server/static && cp -r client/dist server/static", |
|
| 191 | + | "start:single": "cd server && bun run dist/index.js", |
|
| 192 | + | "postinstall": "bun run build:shared && bun run build:server" |
|
| 193 | + | } |
|
| 194 | + | } |
|
| 195 | + | ``` |
|
| 52 | 196 | ||
| 53 | - | ### Install Bun |
|
| 197 | + | ## Development vs Production |
|
| 198 | + | ||
| 199 | + | ### Development (Default bhvr) |
|
| 200 | + | ||
| 201 | + | Use the standard bhvr development workflow: |
|
| 54 | 202 | ||
| 55 | 203 | ```bash |
|
| 56 | - | curl -fsSL https://bun.sh/install | bash |
|
| 204 | + | bun run dev |
|
| 57 | 205 | ``` |
|
| 58 | 206 | ||
| 59 | - | ### Clone Your Project |
|
| 207 | + | This runs: |
|
| 208 | + | ||
| 209 | + | - Client on `http://localhost:5173` (Vite dev server) |
|
| 210 | + | - Server on `http://localhost:3000` (Hono API) |
|
| 211 | + | - Vite proxy forwards `/api` calls to the server |
|
| 212 | + | ||
| 213 | + | ### Production (Single Origin) |
|
| 214 | + | ||
| 215 | + | Build and run from single origin: |
|
| 60 | 216 | ||
| 61 | 217 | ```bash |
|
| 62 | - | sudo mkdir -p /opt/app && sudo chown $USER:$USER /opt/app |
|
| 63 | - | cd /opt/app |
|
| 218 | + | # Build everything and prepare for single origin |
|
| 219 | + | bun run build:single |
|
| 220 | + | ||
| 221 | + | # Start the single origin server |
|
| 222 | + | bun run start:single |
|
| 223 | + | ``` |
|
| 224 | + | ||
| 225 | + | Your app now runs entirely on `http://localhost:3000`. |
|
| 226 | + | ||
| 227 | + | ## Deployment |
|
| 228 | + | ||
| 229 | + | ### Docker |
|
| 230 | + | ||
| 231 | + | ```dockerfile |
|
| 232 | + | FROM oven/bun:latest |
|
| 233 | + | WORKDIR /app |
|
| 234 | + | ||
| 235 | + | # Copy package files |
|
| 236 | + | COPY package.json bun.lockb ./ |
|
| 237 | + | COPY client/package.json ./client/ |
|
| 238 | + | COPY server/package.json ./server/ |
|
| 239 | + | COPY shared/package.json ./shared/ |
|
| 64 | 240 | ||
| 65 | - | git clone https://github.com/your/project.git app |
|
| 66 | - | cd app |
|
| 241 | + | # Install dependencies |
|
| 242 | + | RUN bun install |
|
| 243 | + | ||
| 244 | + | # Copy source code |
|
| 245 | + | COPY . . |
|
| 246 | + | ||
| 247 | + | # Build for single origin |
|
| 248 | + | RUN bun run build:single |
|
| 249 | + | ||
| 250 | + | EXPOSE 3000 |
|
| 251 | + | CMD ["bun", "run", "start:single"] |
|
| 67 | 252 | ``` |
|
| 68 | 253 | ||
| 69 | - | ### Build and Copy |
|
| 254 | + | ### VPS / Bare Metal |
|
| 70 | 255 | ||
| 71 | 256 | ```bash |
|
| 257 | + | # Clone your bhvr project |
|
| 258 | + | git clone <your-repo> my-app && cd my-app |
|
| 259 | + | ||
| 260 | + | # Install and build |
|
| 72 | 261 | bun install |
|
| 73 | - | bun run build |
|
| 74 | - | cp -r client/dist server/dist/client |
|
| 262 | + | bun run build:single |
|
| 263 | + | ||
| 264 | + | # Run (consider using PM2 or systemd for production) |
|
| 265 | + | bun run start:single |
|
| 75 | 266 | ``` |
|
| 76 | 267 | ||
| 77 | - | ### systemd Service |
|
| 268 | + | ### Environment Variables |
|
| 269 | + | ||
| 270 | + | Configure the port and other settings: |
|
| 271 | + | ||
| 272 | + | ```bash |
|
| 273 | + | PORT=8080 bun run start:single |
|
| 274 | + | ``` |
|
| 78 | 275 | ||
| 79 | - | ```ini |
|
| 80 | - | # /etc/systemd/system/app.service |
|
| 81 | - | [Unit] |
|
| 82 | - | Description=bhvr App – Single Origin |
|
| 83 | - | After=network-online.target |
|
| 276 | + | ## File Structure |
|
| 84 | 277 | ||
| 85 | - | [Service] |
|
| 86 | - | User=youruser |
|
| 87 | - | WorkingDirectory=/opt/app |
|
| 88 | - | Environment=YOUR_ENV_VARS |
|
| 89 | - | ExecStart=/home/youruser/.bun/bin/bun run server/dist/server/src/index.js |
|
| 90 | - | Restart=always |
|
| 91 | - | RestartSec=5 |
|
| 278 | + | After building for single origin, your bhvr project structure looks like: |
|
| 92 | 279 | ||
| 93 | - | [Install] |
|
| 94 | - | WantedBy=multi-user.target |
|
| 280 | + | ``` |
|
| 281 | + | . |
|
| 282 | + | ├── client/ |
|
| 283 | + | │ ├── dist/ # Built React app |
|
| 284 | + | │ └── src/ |
|
| 285 | + | ├── server/ |
|
| 286 | + | │ ├── dist/ |
|
| 287 | + | │ │ └── index.js # Built Hono server |
|
| 288 | + | │ ├── static/ # Copied from client/dist |
|
| 289 | + | │ └── src/ |
|
| 290 | + | ├── shared/ |
|
| 291 | + | │ ├── dist/ # Built shared types |
|
| 292 | + | │ └── src/ |
|
| 293 | + | └── package.json |
|
| 95 | 294 | ``` |
|
| 96 | 295 | ||
| 97 | - | ```bash |
|
| 98 | - | sudo systemctl daemon-reload |
|
| 99 | - | sudo systemctl enable --now app |
|
| 296 | + | ## CORS Configuration |
|
| 297 | + | ||
| 298 | + | Since single origin serves everything from the same origin, **CORS is not required** for production. However, you might want to keep it for development flexibility: |
|
| 299 | + | ||
| 300 | + | **Production (Single Origin):** |
|
| 301 | + | ||
| 302 | + | - React app and API served from same origin (e.g., `https://yourapp.com`) |
|
| 303 | + | - All requests are same-origin |
|
| 304 | + | - CORS not needed |
|
| 305 | + | ||
| 306 | + | **Development:** |
|
| 307 | + | ||
| 308 | + | - Vite proxy handles cross-origin requests automatically |
|
| 309 | + | - CORS still optional due to proxy, but useful for: |
|
| 310 | + | - Testing API directly in browser/tools |
|
| 311 | + | - Alternative development setups |
|
| 312 | + | - Third-party integrations during development |
|
| 313 | + | ||
| 314 | + | **To remove CORS for production**, you can conditionally apply it: |
|
| 315 | + | ||
| 316 | + | ```typescript |
|
| 317 | + | // Only use CORS in development |
|
| 318 | + | if (process.env.NODE_ENV !== "production") { |
|
| 319 | + | app.use(cors()); |
|
| 320 | + | } |
|
| 100 | 321 | ``` |
|
| 101 | 322 | ||
| 102 | - | ### Expose It |
|
| 323 | + | Or remove the `app.use(cors())` line entirely if you don't need development flexibility. |
|
| 103 | 324 | ||
| 104 | - | You can expose the server using: |
|
| 325 | + | ## Key Benefits |
|
| 105 | 326 | ||
| 106 | - | - [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/) |
|
| 107 | - | - [Caddy](https://caddyserver.com) |
|
| 108 | - | - [Tailscale Funnel](https://tailscale.com/funnel/) |
|
| 327 | + | - **Simplified deployment**: One process, one port, one URL |
|
| 328 | + | - **No CORS complexity**: Frontend and API share the same origin |
|
| 329 | + | - **Maintains bhvr workflow**: Still use `bun run dev` for development |
|
| 330 | + | - **Type safety preserved**: All bhvr type sharing continues to work |
|
| 331 | + | - **Resource efficient**: Perfect for small VPS, Raspberry Pi, or containers |
|
| 109 | 332 | ||
| 110 | - | Each of these lets you skip dealing with NAT, SSL, and port forwarding. |
|
| 333 | + | ## Troubleshooting |
|
| 111 | 334 | ||
| 112 | - | ## Summary |
|
| 335 | + | **API calls fail in development?** |
|
| 336 | + | ||
| 337 | + | - Ensure Vite proxy is configured in `client/vite.config.ts` |
|
| 338 | + | - Check that your server is running on port 3000 |
|
| 113 | 339 | ||
| 114 | - | Single origin deployments are a great fit when you want everything bundled into one runtime and served from a single point of entry. |
|
| 340 | + | **404 errors on page refresh?** |
|
| 115 | 341 | ||
| 116 | - | They’re fast, minimal, and don’t require any orchestration. |
|
| 342 | + | - The `serveStatic` catchall should handle SPA routing automatically |
|
| 343 | + | - Verify client files are copied to `server/static/` |
|
| 344 | + | ||
| 345 | + | **Build fails?** |
|
| 346 | + | ||
| 347 | + | - Run `bun run build` first to ensure shared types are available |
|
| 348 | + | - Check that all bhvr workspaces install correctly |
|
| 349 | + | ||
| 350 | + | ## Summary |
|
| 351 | + | ||
| 352 | + | Single origin deployment transforms your bhvr project from a multi-port development setup into a production-ready single process application, while preserving all the type safety and development experience that makes bhvr great. |
|
| 117 | 353 | ||
| 118 | 354 | ## More Resources |
|
| 119 | 355 |