Skip to content

Single Origin Deployment

Serve both your frontend and API from the same process, same port, and same origin—ideal for fullstack apps where simplicity matters.

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.

Perfect for:

  • VPS deployments
  • Raspberry Pis
  • Home servers
  • Docker containers
  • Projects where you want one URL to rule them all

Prerequisites

This guide assumes you have a bhvr project set up. If not, start here:

bun create bhvr@latest my-app
cd my-app
Getting Started with bhvr

What Is Single Origin?

Instead of running your client and server separately (the default bhvr setup), single origin serves everything from one process:

Default bhvr setup:
  • Client runs on port 5173 (Vite dev server)
  • Server runs on port 3000 (Hono API)
  • Requires CORS for communication
Single origin setup:
  • Everything runs on port 3000
  • Hono serves both API routes and static React files
  • No CORS needed

Configuration

1. Update Your Hono Server

Modify server/src/index.ts to serve static files alongside your API:

import { Hono } from "hono";
import { cors } from "hono/cors";
import { serveStatic } from "hono/bun";
import type { ApiResponse } from "shared/dist";
 
const app = new Hono();
 
// CORS is optional for single origin deployment
// Keep for development flexibility, remove for production if desired
app.use(cors());
 
// Your existing API routes - keep the /api prefix for clarity
app.get("/api", (c) => {
  return c.text("Hello Hono!");
});
 
app.get("/api/hello", async (c) => {
  const data: ApiResponse = {
    message: "Hello BHVR!",
    success: true,
  };
  return c.json(data, { status: 200 });
});
 
// Add more API routes here with /api prefix
// app.get('/api/users', ...)
// app.post('/api/data', ...)
 
// Serve static files for everything else
app.use("*", serveStatic({ root: "./static" }));
 
app.get("*", async (c, next) => {
  return serveStatic({ root: "./static", path: "index.html" })(c, next);
});
 
const port = parseInt(process.env.PORT || "3000");
 
export default {
  port,
  fetch: app.fetch,
};
 
console.log(`🦫 bhvr server running on port ${port}`);

2. Update Your React Client

Modify client/src/App.tsx to use relative API paths:

import { useState } from "react";
import beaver from "./assets/beaver.svg";
import { ApiResponse } from "shared";
import "./App.css";
 
function App() {
  const [data, setData] = useState<ApiResponse | undefined>();
 
  async function sendRequest() {
    try {
      // Use relative path - works in both dev and production
      const req = await fetch("/api/hello");
      const res: ApiResponse = await req.json();
      setData(res);
    } catch (error) {
      console.log(error);
    }
  }
 
  return (
    <>
      <div>
        <a href='https://github.com/stevedylandev/bhvr' target='_blank'>
          <img src={beaver} className='logo' alt='beaver logo' />
        </a>
      </div>
      <h1>bhvr</h1>
      <h2>Bun + Hono + Vite + React</h2>
      <p>A typesafe fullstack monorepo</p>
      <div className='card'>
        <button onClick={sendRequest}>Call API</button>
        {data && (
          <pre className='response'>
            <code>
              Message: {data.message} <br />
              Success: {data.success.toString()}
            </code>
          </pre>
        )}
      </div>
      <p className='read-the-docs'>Click the beaver to learn more</p>
    </>
  );
}
 
export default App;

3. Configure Vite for Development

Update client/vite.config.ts to proxy API calls during development:

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import path from "path";
 
export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      "@client": path.resolve(__dirname, "./src"),
      "@server": path.resolve(__dirname, "../server/src"),
      "@shared": path.resolve(__dirname, "../shared/src"),
    },
  },
  server: { 
    proxy: { 
      "/api": { 
        target: "http://localhost:3000", 
        changeOrigin: true, 
      }, 
    }, 
  }, 
});

4. Add Single Origin Scripts

Add these scripts to your root package.json (alongside the existing bhvr scripts):

{
  "scripts": {
    "dev:client": "cd client && bun run dev",
    "dev:server": "cd server && bun run dev",
    "dev:shared": "cd shared && bun run dev",
    "dev": "concurrently \"bun run dev:shared\" \"bun run dev:server\" \"bun run dev:client\"",
    "build:client": "cd client && bun run build",
    "build:shared": "cd shared && bun run build",
    "build:server": "cd server && bun run build",
    "build": "bun run build:shared && bun run build:client",
    "build:single": "bun run build && bun run copy:static && bun run build:server",
    "copy:static": "rm -rf server/static && cp -r client/dist server/static",
    "start:single": "cd server && bun run dist/index.js",
    "postinstall": "bun run build:shared && bun run build:server"
  }
}

Development vs Production

Development (Default bhvr)

Use the standard bhvr development workflow:

bun run dev

This runs:

  • Client on http://localhost:5173 (Vite dev server)
  • Server on http://localhost:3000 (Hono API)
  • Vite proxy forwards /api calls to the server

Production (Single Origin)

Build and run from single origin:

# Build everything and prepare for single origin
bun run build:single
 
# Start the single origin server
bun run start:single

Your app now runs entirely on http://localhost:3000.

Deployment

Docker

FROM oven/bun:latest
WORKDIR /app
 
# Copy package files
COPY package.json bun.lock ./
COPY client/package.json ./client/
COPY server/package.json ./server/
COPY shared/package.json ./shared/
 
# Copy source code
COPY . .
 
# Install dependencies
RUN bun install
 
# Build for single origin
RUN bun run build:single
 
EXPOSE 3000
CMD ["bun", "run", "start:single"]

VPS / Bare Metal

# Clone your bhvr project
git clone <your-repo> my-app && cd my-app
 
# Install and build
bun install
bun run build:single
 
# Run (consider using PM2 or systemd for production)
bun run start:single

Environment Variables

Configure the port and other settings:

PORT=8080 bun run start:single

File Structure

After building for single origin, your bhvr project structure looks like:

.
├── client/
│   ├── dist/           # Built React app
│   └── src/
├── server/
│   ├── dist/
│   │   └── index.js    # Built Hono server
│   ├── static/         # Copied from client/dist
│   └── src/
├── shared/
│   ├── dist/           # Built shared types
│   └── src/
└── package.json

CORS Configuration

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:

Production (Single Origin):
  • React app and API served from same origin (e.g., https://yourapp.com)
  • All requests are same-origin
  • CORS not needed
Development:
  • Vite proxy handles cross-origin requests automatically
  • CORS still optional due to proxy, but useful for:
    • Testing API directly in browser/tools
    • Alternative development setups
    • Third-party integrations during development

To remove CORS for production, you can conditionally apply it:

// Only use CORS in development
if (process.env.NODE_ENV !== "production") {
  app.use(cors());
}

Or remove the app.use(cors()) line entirely if you don't need development flexibility.

Key Benefits

  • Simplified deployment: One process, one port, one URL
  • No CORS complexity: Frontend and API share the same origin
  • Maintains bhvr workflow: Still use bun run dev for development
  • Type safety preserved: All bhvr type sharing continues to work
  • Resource efficient: Perfect for small VPS, Raspberry Pi, or containers

Troubleshooting

API calls fail in development?
  • Ensure Vite proxy is configured in client/vite.config.ts
  • Check that your server is running on port 3000
404 errors on page refresh?
  • The serveStatic catchall should handle SPA routing automatically
  • Verify client files are copied to server/static/
Build fails?
  • Run bun run build first to ensure shared types are available
  • Check that all bhvr workspaces install correctly

Summary

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.

More Resources

Getting Started with bhvr