docs: Enhance single-origin deployment guide with detailed configuration steps and examples 1f00130a
iammatthias · 2025-05-24 08:48 1 file(s) · +295 −59
docs/pages/deployment/single-origin.mdx +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