# bhvr > A stack made for the open web ## Get started bhvr is a framework used to build apps that are not tied down to a single provider, and each piece can be deployed in multiple places. ### Quickstart :::steps #### Create a New Project To start using bhvr make sure you have [Bun](https://bun.sh) installed first, then run the following command. ```bash [terminal] bun create bhvr@latest ``` The stack is composed of the following: ``` . ├── client/ # React frontend ├── server/ # Hono backend ├── shared/ # Shared TypeScript definitions │ └── src/types/ # Type definitions used by both client and server └── package.json # Root package.json with workspaces ``` #### Start Up Dev Server Once you have created your project you can `cd` into it and run the dev server ```bash [terminal] bun run dev ``` This will spin up dev servers for the `server`, `client`, and `shared` packages Try updating the API endpoints in the `server` package ```typescript [server/src/index.ts] import { Hono } from 'hono' import { cors } from 'hono/cors' import type { ApiResponse } from 'shared/dist' const app = new Hono() app.use(cors()) app.get('/', (c) => { return c.text('Hello Hono!') }) app.get('/hello', async (c) => { const data: ApiResponse = { message: "Hello BHVR!", success: true } return c.json(data, { status: 200 }) }) export default app ``` Also try editing the React app in `client` ```tsx [client/src/App.tsx] import { useState } from 'react' import beaver from './assets/beaver.svg' import { ApiResponse } from 'shared' import './App.css' const SERVER_URL = import.meta.env.VITE_SERVER_URL || "http://localhost:3000" function App() { const [data, setData] = useState() async function sendRequest() { try { const req = await fetch(`${SERVER_URL}/hello`) const res: ApiResponse = await req.json() setData(res) } catch (error) { console.log(error) } } return ( <>
beaver logo

bhvr

Bun + Hono + Vite + React

A typesafe fullstack monorepo

{data && (
            
            Message: {data.message} 
Success: {data.success.toString()}
)}

Click the beaver to learn more

) } export default App ``` #### Build Project Once you have your project ready to go you can use the build command to build the `client` and `shared` packages ```bash [terminal] bun run build ``` From there you can select multiple [deployment options](/deployment/client/cloudflare-pages) ::: ### Manual Setup There are few other ways you can get started with bhvr outside of the CLI #### Clone or Use GitHub Template If you visit the [bhvr repo](https://github.com/stevedylandev/bhvr) you can click the "Use This Template" button in the top right. ![screenshot](https://cdn.stevedylan.dev/ipfs/bafybeicf2phwxwkqwl7uhr4awdrd5h7a37mqd7eay3czbpaldozjze3noa) Alternatively you can clone the template ```bash [terminal] git clone https://github.com/stevedylandev/bhvr ``` #### Create from Scratch To recreate bhvr from scratch you can do the following :::steps ##### Install Bun ```bash [terminal] curl -fsSL https://bun.sh/install | bash ``` ##### Setup Project ```bash [terminal] mkdir bhvr cd bhvr bun init -y rm index.ts mkdir shared ``` ##### Update Root Files Update `tsconfig.json` with the following settings ```json { "compilerOptions": { // Environment setup & latest features "lib": ["ESNext", "DOM", "DOM.Iterable"], "target": "ESNext", "module": "ESNext", "moduleDetection": "force", "jsx": "react-jsx", "allowJs": true, // Path resolution "baseUrl": "./", "paths": { "@server/*": ["./server/src/*"], "@client/*": ["./client/src/*"], "@shared/*": ["./shared/src/*"] }, // Module resolution "moduleResolution": "bundler", "allowSyntheticDefaultImports": true, "esModuleInterop": true, "verbatimModuleSyntax": true, // Strictness and best practices "strict": true, "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, "noUncheckedIndexedAccess": true, "experimentalDecorators": true, // Output control "skipLibCheck": true, // Optional strict flags (disabled by default) "noUnusedLocals": false, "noUnusedParameters": false, "noPropertyAccessFromIndexSignature": false } } ``` Update the `package.json` with the following contents ```json { "name": "bhvr", "version": "0.3.0", "description": "A monorepo template built with Bun, Hono, Vite, and React", "license": "MIT", "workspaces": [ "./server", "./client", "./shared" ], "scripts": { "dev": "turbo dev", "dev:client": "turbo dev --filter=client", "dev:server": "turbo dev --filter=server", "build": "turbo build", "build:client": "turbo build --filter=client", "build:server": "turbo build --filter=server", "lint": "turbo lint", "type-check": "turbo type-check", "test": "turbo test", "postinstall": "turbo build --filter=shared --filter=server" }, "keywords": [ "bun", "hono", "react", "vite", "monorepo" ], "devDependencies": { "bun-types": "latest", "turbo": "^2.5.5" }, "peerDependencies": { "typescript": "^5.0.0" } } ``` ##### Setup Client Setup Vite with your preferences ```bash [terminal] bun create vite@latest client cd client ``` Update the `package.json` file ```json { "name": "client", "private": true, "version": "0.0.1", "type": "module", "scripts": { "dev": "vite", "build": "tsc -b && vite build", "lint": "eslint .", "preview": "vite preview" }, "dependencies": { "react": "^19.0.0", "react-dom": "^19.0.0", "shared": "workspace:*", // [!code focus] "server": "workspace:*" // [!code focus] }, "devDependencies": { "@eslint/js": "^9.22.0", "@types/node": "^22.15.2", // [!code focus] "@types/react": "^19.0.10", "@types/react-dom": "^19.0.4", "@vitejs/plugin-react": "^4.3.4", "eslint": "^9.22.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.19", "globals": "^16.0.0", "typescript": "~5.7.2", "typescript-eslint": "^8.26.1", "vite": "^6.3.1" } } ``` Update the `tsconfig.json` file ```json { "extends": "../tsconfig.json", // [!code focus] "files": [], "references": [{ "path": "./tsconfig.node.json" }, { "path": "./tsconfig.app.json"}] } ``` Update the `vite.config.ts` file ```typescript import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import path from 'path' // [!code focus] export default defineConfig({ plugins: [react()], resolve: { // [!code focus] alias: { // [!code focus] "@client": path.resolve(__dirname, "./src"), // [!code focus] "@server": path.resolve(__dirname, "../server/src"), // [!code focus] "@shared": path.resolve(__dirname, "../shared/src") // [!code focus] } } }) ``` ##### Setup Server Create `server` repo with Hono ```bash [terminal] bun create hono@latest server cd server ``` Update the `tsconfig.json` file ```json { "extends": "../tsconfig.json", // [!code focus] "compilerOptions": { // Environment settings "lib": ["ESNext"], "target": "ESNext", "module": "ESNext", "jsx": "react-jsx", "jsxImportSource": "hono/jsx", // Types "types": ["bun-types"], // Output settings "declaration": true, // [!code focus] "outDir": "dist", // [!code focus] "noEmit": false, // [!code focus] "emitDecoratorMetadata": true, // [!code focus] // Module resolution "moduleResolution": "bundler", "allowImportingTsExtensions": false }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] } ``` Update the `package.json` file ```json { "name": "server", "version": "0.0.1", "main": "dist/index.js", // [!code focus] "types": "dist/index.d.ts", // [!code focus] "scripts": { "build": "tsc", "dev": "bun run --hot src/index.ts" }, "dependencies": { "hono": "^4.7.7", "shared": "workspace:*" // [!code focus] }, "devDependencies": { "@types/bun": "latest" } } ``` ##### Setup Shared Initialize the directory ```bash [terminal] cd shared bun init -y mkdir src mv index.ts src/index.ts ``` Update the `tsconfig.json` file ```json { "extends": "../tsconfig.json", // [!code focus] "compilerOptions": { // Environment setup "lib": ["ESNext"], "target": "ESNext", "module": "ESNext", // Output configuration "declaration": true, // [!code focus] "outDir": "./dist", // [!code focus] "noEmit": false, // [!code focus] // Type checking "strict": true, "skipLibCheck": true, // Additional checks "noFallthroughCasesInSwitch": true, "noUncheckedIndexedAccess": true }, "include": ["src/**/*"], // [!code focus] "exclude": ["node_modules", "**/*.test.ts", "dist"] // [!code focus] } ``` Update the `package.json` file ```json { "name": "shared", "version": "0.0.1", "main": "dist/index.js", "types": "dist/index.d.ts", "scripts": { "build": "tsc", "dev": "tsc --watch" }, "devDependencies": { "typescript": "^5.2.2" } } ``` ##### Start Up bhvr 🦫 ```bash [terminal] bun install bun dev ``` ::: ## Why bhvr? I get it. Why another typescript framework for web apps? Aren't we just heaping fuel to the already massive dumpster fire? While that might be true, the reality is the web is continuing to be built on JavaScript, and generally written in TypeScript. While that reality stays true, there are some concerning pieces of our ecosystem that create more walled gardens and vendor lock-in. SSR is notorious for this as it prevents web apps from being portable, where you can easily host a piece of your stack somewhere else with little to no effort. That old age was more commonly known as Jamstack (Javascript APIs and Markdown), and in that period we saw a renaissance of web apps that were lightweight, efficient, and most importantly open and portable. ![image](https://dweb.mypinata.cloud/ipfs/bafybeiglk63npgshe6teogi5o5tljcmyx2ifddkwion4znbfpiceehnnn4) The tides are turning and SSR is not always everyone's top pick these days. Developers from the Jamstack era long for the old times again, and new developers experience the frustration that comes with complicated frameworks such as Next.js. bhvr was built on a whim to fill the void Next.js leaves most developers, and it's mission now is to become a stack for the open web. import { Button } from 'vocs/components' ## `client` bhvr uses Vite + React as its default client side template, as React has become one of the industry defaults with a vast amount of ecosystem support. With that said you can absolutely replace it with your client of choice by [following the guide](). We highly recommend using [Vite](https://vite.dev) as your bundler since it too has lots of ecosystem suppport and is very lightweight. ### Basics Just in case you haven't used Vite + React much here are some basics you may want to keep in mind. #### Client Side Only Unlike Next.js, Vite + React is client side only. This means any kind of environment variable used here will be publicly accessible, which is why we have a [`server`](/packages/server) package to keep those secure. This can take some getting used to when building an application and how you might fetch data. A classic way to achieve an "on-load" API call to your server is through a `useEffect`. ```tsx App.tsx import { useState, useEffect } from 'react'; function UserProfile() { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { // Define an async function inside useEffect async function fetchUser() { try { setLoading(true); // Make the API call to your server const response = await fetch(`${import.meta.env.SERVER_URL}`); // Handle non-200 responses if (!response.ok) { throw new Error(`Error: ${response.status}`); } const data = await response.json(); setUser(data); setError(null); } catch (err) { setError(err.message); setUser(null); } finally { setLoading(false); } } // Call the function fetchUser(); // If needed, you can return a cleanup function return () => { // Any cleanup code (if needed) }; }, []); // Empty dependency array means this runs once on mount if (loading) return
Loading...
; if (error) return
Error: {error}
; if (!user) return
No user data found
; return (

User Profile

Name: {user.name}

Email: {user.email}

); } ``` #### Environment Variables Despite environment variables being public in this setup, they still come in handy for things like working with your local server URL vs your deployed instance. It's best practice to keep your variables in a `.env.local` file with the following format, taking special note that they need to start with `VITE_`. ``` // [!code word:VITE] VITE_MY_VAR=value ``` To use them inside your app be sure to use this Vite formatting ```typescript const variable = import.meta.env.VITE_MY_VAR ``` #### Check the Config Vite's config file `vite.config.ts` is worth exploring as it can provide some extra options and plugins. ### Styles When creating a new bhvr project you can use the CLI to specify the CSS template you want ```bash [terminal] bun create bhvr@latest --template default # Classic CSS bun create bhvr@latest --template tailwind # Tailwind installed and setup bun create bhvr@latest --template shadcn # Tailwind + Shadcn/ui component setup ``` ### Routing There are serveral ways to handle routing in your client app, but few come close to [React Router](http://reactrouter.com). Setting it up is quite simple and intuitive. :::steps #### Install `react-router` Make sure you're inside the `client` directory and then install `react-router` ```bash [terminal] bun add react-router ``` #### Setup Router You can do this inside `main.tsx` or `App.tsx`, I prefer the latter. All you have to do is import the `BrowserRouter`, `Routes`, then declare your routes with the components they go to inside. ```tsx App.tsx import { BrowserRouter, Routes, Route } from "react-router"; import Home from "./components/Home"; function App() { return ( } /> ); } export default App ``` #### Use Dynamic Routes If you want to have a dynamic route with a path param you can set it up in your `Routes` like so ```tsx import { BrowserRouter, Routes, Route } from "react-router"; import Home from "./components/Home"; import Post from "./components/Post"; function App() { return ( } /> } /> ); } export default App ``` Then inside the component you can access those params with `useParams` ```typescript import { useParams } from "react-router"; function Post(){ const { slug } = useParams() return ( <>

Post {slug}

) } export default Post; ``` ::: ### Deployment import { Button } from "vocs/components"; ## `server` This package lets us build an API or connect databases and other tools that require a server environment. bhvr uses [Hono](https://hono.dev) to power the API as it's simple, easy to use, and has a lot of ecosystem plugins and adoption. It's similar to express but lighter and more modern. :::tip Check out the [official Hono documentation](https://hono.dev) for a full reference ::: ### Basics You can declare routes in your API like so ```typescript import { Hono } from "hono"; const app = new Hono(); app.get("/", (c) => { return c.text("Hello Hono!"); }); export default app; ``` Hono also makes it easy to add in path parameters ```typescript app.get("/user/:name", async (c) => { const name = c.req.param("name"); // ... }); ``` or multiple parameters ```typescript app.get("/posts/:id/comment/:comment_id", async (c) => { const { id, comment_id } = c.req.param(); // ... }); ``` The `(c)` in Hono is the `Context` which has loads of primary features of your API. **Access a Request** ```typescript app.get("/hello", (c) => { const userAgent = c.req.header("User-Agent"); // ... }); ``` **Return JSON or HTML** ```typescript app.get("/api", (c) => { return c.json({ message: "Hello!" }); }); ``` ```typescript app.get("/", (c) => { return c.html("

Hello! Hono!

"); }); ``` **Access an ENV** ```typescript // Type definition to make type inference type Bindings = { MY_KV: KVNamespace; }; const app = new Hono<{ Bindings: Bindings }>(); // Environment object for Cloudflare Workers app.get("/", async (c) => { c.env.MY_KV.get("my-key"); // ... }); ``` ### RPC One of the unique built in features of Hono is it's RPC. With this enabled you can create a Hono client in your frontend and get automatic type safety without needing to import or export types from `shared`. This is not enabled by default to help keep an unbiased template starter, but when creating a new bhvr project it is easy to enable. ```bash [terminal] bun create bhvr@latest --rpc ``` This will setup your API with the following code, and the key being the use of `const routes` and exporting the `AppType` ```typescript src/index.ts import { Hono } from "hono"; import { cors } from "hono/cors"; import type { ApiResponse } from "shared/dist"; const app = new Hono(); app.use(cors()); const routes = app .get("/", (c) => { //[!code focus] return c.text("Hello Hono!"); }) .get("/hello", async (c) => { const data: ApiResponse = { message: "Hello BHVR!", success: true, }; return c.json(data, { status: 200 }); }); export type AppType = typeof routes; // [!code focus] export default app; ``` In your `client` code Hono is installed as a dependency, and the `hc` client is imported and initialized. The `AppType` is also used so we can build types with it. ```typescript src/App.tsx import { useState } from 'react' import beaver from './assets/beaver.svg' import type { AppType } from 'server' // [!code focus] import { hc } from 'hono/client' // [!code focus] import './App.css' const SERVER_URL = import.meta.env.VITE_SERVER_URL || "http://localhost:3000" const client = hc(SERVER_URL); // [!code focus] type ResponseType = Awaited>; // [!code focus] function App() { const [data, setData] = useState> | undefined>() async function sendRequest() { try { const res = await client.hello.$get() if (!res.ok) { console.log("Error fetching data") return } const data = await res.json() setData(data) } catch (error) { console.log(error) } } return ( <> {/* JSX markup...*/} ) } export default App ``` ### DB Connections There are lots of options out there which can be simple as installing some packages and setting up API keys like [Supabase](https://supabase.com). You can also install ORM clients to handle raw connections like [Drizzle](https://orm.drizzle.team) or [Prisma](https://prisma.io/orm). Since Hono works great with Cloudflare Workers I would also highly recommend checking out [D1](https://developers.cloudflare.com/d1) as it can be easily accessed through the Hono context. ```typescript import { Hono } from "hono"; // This ensures c.env.DB is correctly typed type Bindings = { DB: D1Database; }; const app = new Hono<{ Bindings: Bindings }>(); // Accessing D1 is via the c.env.YOUR_BINDING property app.get("/query/users/:id", async (c) => { const userId = c.req.param("id"); try { let { results } = await c.env.DB.prepare("SELECT * FROM users WHERE user_id = ?") .bind(userId) .all(); return c.json(results); } catch (e) { return c.json({ err: e.message }, 500); } }); // Export our Hono app: Hono automatically exports a // Workers 'fetch' handler for you export default app; ``` ### Cloudflare Workers One of the best ways to use Hono is with Cloudflare Workers. They're cheap, pretty easy to use, and can link to other Cloudflare services like KVs or Databases. However there are some differences with how you use Hono if you take the Worker route, so we'll point out some tips here. #### Environment Variables Cloudlfare has both public and private variables, and can only be accessed through the Hono Context. This means if you have a function in another file or folder using something like `process.env.MY_SECRET` it's not going to work. Instead you need to put your environemnt names as Bindings to make everything typesafe. ```typescript type Bindings = { MY_BUCKET: R2Bucket; USERNAME: string; PASSWORD: string; }; const app = new Hono<{ Bindings: Bindings }>(); // Access to environment values app.put("/upload/:key", async (c, next) => { const key = c.req.param("key"); await c.env.MY_BUCKET.put(key, c.req.body); return c.text(`Put ${key} successfully!`); }); ``` To set these variables, public ones can be put inside the `wrangler.jsonc` or `wrangler.toml` file. ```jsonc { "$schema": "node_modules/wrangler/config-schema.json", "name": "server", "main": "src/index.ts", "compatibility_date": "2025-05-07", // "compatibility_flags": [ // "nodejs_compat" // ], "vars": { "MY_VAR": "my-variable", }, "kv_namespaces": [ { "binding": "MY_KV_NAMESPACE", "id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", }, ], // "r2_buckets": [ // { // "binding": "MY_BUCKET", // "bucket_name": "my-bucket" // } // ], // "d1_databases": [ // { // "binding": "MY_DB", // "database_name": "my-database", // "database_id": "" // } // ], // "ai": { // "binding": "AI" // }, // "observability": { // "enabled": true, // "head_sampling_rate": 1 // } } ``` Secret variables can be used in a test env by storing them in a `.dev.vars` file ``` MY_SECRET = "SOME_SECRET" ``` To use secret variables for deployment you can use the Cloudflare dashboard or use the Wrangler CLI ```bash [terminal] bunx wrangler secret put MY_SECRET # Prompt: Enter your secret to have it encrypted on Cloudflare ``` :::tip Read the [Hono documentation](https://hono.dev/docs/getting-started/cloudflare-workers) on Workers for more info ::: ### Deployment ## `shared` The `shared` package is primarily used for types that you may want to pass between server and client, but it can also be used for functions or libraries. When you create a project with `bun create bhvr@latest` this is the structure of the package. ``` shared ├── package.json ├── src │   ├── index.ts │   └── types │   └── index.ts └── tsconfig.json ``` It will resemble a barrel export pattern, so the contents of `src/index.ts` are the following ```typescript src/index.ts export * from "./types" ``` By running either `bun run build` or `bun run dev` the types will be compiled and exported from the `dist` build folder and can be used in `client` or `server`. ## Cloudflare Workers One of the simplest ways to deploy your `server` is through Cloudflare Workers, which are lightweight, fast, and very inexpensive. ### Deployment :::steps #### Login to Cloudlfare Using the `wrangler` CLI login to your Cloudflare account to authorize it ```bash [!terminal] bunx wrangler login ``` #### Add `wrangler.jsonc` File Create a new file called `wrangler.jsonc` in the root of your `server` package and paste in the following template. ```jsonc { "$schema": "node_modules/wrangler/config-schema.json", "name": "server", "main": "src/index.ts", "compatibility_date": "2025-05-25" // "compatibility_flags": [ // "nodejs_compat" // ], // "vars": { // "MY_VAR": "my-variable" // }, // "kv_namespaces": [ // { // "binding": "MY_KV_NAMESPACE", // "id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" // } // ], // "r2_buckets": [ // { // "binding": "MY_BUCKET", // "bucket_name": "my-bucket" // } // ], // "d1_databases": [ // { // "binding": "MY_DB", // "database_name": "my-database", // "database_id": "" // } // ], // "ai": { // "binding": "AI" // }, // "observability": { // "enabled": true, // "head_sampling_rate": 1 // } } ``` #### Update `package.json` Files Add the following items to your `server/package.json` file ```json { "name": "server", "version": "0.0.1", "main": "dist/index.js", "types": "dist/index.d.ts", "scripts": { "build": "tsc", "dev": "bun run --hot src/index.ts", // [!code --] "dev": "wrangler dev", // [!code ++] "deploy": "wrangler deploy --minify", // [!code ++] "cf-typegen": "wrangler types --env-interface CloudflareBindings" // [!code ++] }, "dependencies": { "hono": "^4.7.7", "shared": "workspace:*" }, "devDependencies": { "@types/bun": "latest", "wrangler": "^4.4.0" // [!code ++] } } ``` Then update `scripts` section of the root `package.json` for your bhvr project ```json "scripts": { "dev": "turbo dev", "dev:client": "turbo dev --filter=client", "dev:server": "turbo dev --filter=server", "build": "turbo build", "build:client": "turbo build --filter=client", "build:server": "turbo build --filter=server", "deploy:server": "cd server && bun run deploy", // [!code ++] "lint": "turbo lint", "type-check": "turbo type-check", "test": "turbo test", "postinstall": "turbo build --filter=shared --filter=server" }, ``` #### Deploy Install dependencies for your updated `server/package.json` then run the deploy command ```bash [!terminal] bun install bun run deploy:server ``` ::: :::tip [Check out the Cloudflare Docs for more info and tips](https://developers.cloudflare.com/workers/) ::: ## Railway For a simple Bun server deployment you can use Railway by changing just a few lines of code. :::steps #### Update `app` export Update the code inside `server/src/index.ts` and use the export below which gives Railway access to the host and port. ```typescript [srver/src/index.ts] import { Hono } from 'hono' import { cors } from 'hono/cors' import type { ApiResponse } from 'shared/dist' const app = new Hono() app.use(cors()) app.get('/', (c) => { return c.text('Hello Hono!') }) app.get('/hello', async (c) => { const data: ApiResponse = { message: "Hello BHVR!", success: true } return c.json(data, { status: 200 }) }) export default app; // [!code --] export default { // [!code ++] port: Number(process.env.PORT) || 3000, // [!code ++] hostname: '0.0.0.0', // [!code ++] fetch: app.fetch, // [!code ++] }; // [!code ++] ``` #### Add start command Inside the root `package.json` add a new `start` command that will be used by Railway for deployment ```json [package.json] // Rest of package.json "scripts": { "dev": "turbo dev", "dev:client": "turbo dev --filter=client", "dev:server": "turbo dev --filter=server", "build": "turbo build", "build:client": "turbo build --filter=client", "build:server": "turbo build --filter=server", "lint": "turbo lint", "type-check": "turbo type-check", "test": "turbo test", "postinstall": "turbo build --filter=shared --filter=server", "start": "bun run server/dist/index.js" // [!code focus] }, ``` #### Deploy on Railway Login to your Railway account and create new project from your Git source ![new project](https://cdn.bhvr.dev/railway-new-project.png) After selecting the repo with your changes from the previous steps it should automatically deploy your instance! To access it from a public URL go to the instance settings, and under `Networking` click `Generate Domain` ![generate domain](https://cdn.bhvr.dev/railway-new-domain.png) ::: :::tip With some tweaking you could use this same approach to deploy your entire app with the [single origin deployment guide](/deployment/single-origin/vps-docker) ::: ## Cloudflare Pages A great way to host your bhvr client, especially since your server can be hosted with Cloudflare Workers. There are two primary ways you can host your client through Pages: * Git Integration * Manual Upload :::note Once you have chosen one of these methods you will not be able to switch between the two! Doing so will require deleting the site and starting over. ::: ### Git Integration ::::steps #### Create a New Pages Project Login to your Cloudflare account and then from the sidebar select `Compute (Workers)` and click the `Create` button in the top right. Select the pages tab and then select the Git repository option. #### Setup Your Project Connect your choice of Git account and then select the repo for your bhvr project. Once select make sure you have the following items setup accordingly under the build settings: * Framework Preset: `React (Vite)` * Build command: `bun install && bun run build` * Build output directory: `dist` * Root directory (advanced): `client` * Environment Variables: `BUN_VERSION=1.2.14` (use the latest version) ![dashboard screenshot](https://dweb.mypinata.cloud/ipfs/bafkreicjxhuj6p3kiyg7u6nhoy4z3lvttwka2hxrl5vmrh5ennil6es57q) #### Save and Deploy Once you have those settings entered you can just click the "Save and Deploy" button in the bottom right, and you're done! Anytime you push a new commit it will deploy a new version of the site, including previews for branches. :::tip [Check out the Cloudflare Docs for more info and tips](https://developers.cloudflare.com/pages/get-started/git-integration/) ::: :::: ### Direct Upload ::::steps #### Login to Cloudlfare Using the `wrangler` CLI login to your Cloudflare account to authorize it ```bash [!terminal] bunx wrangler login ``` #### Setup Project First make sure you have built the client ```bash [!terminal] bun run build ``` Then use the `wrangler` CLI to create a new empty project in Cloudflare ```bash [!terminal] bunx wrangler pages project create ``` #### Deploy Lastly run the following command to directly upload the `dist` build folder of your `client` ```bash [!terminal] bunx wrangler pages deploy client/dist ``` You can automate this into a script by adding it to your root `package.json` ```json { "scripts": { // Other scripts "deploy:client": "bun run build:client && bunx wrangler pages deploy client/dist" } } ``` :::tip [Check out the Cloudflare Docs for more info and tips](https://developers.cloudflare.com/pages/get-started/direct-upload/) ::: :::: ## GitHub Pages Since the bhvr client is a simple static site you can deploy it just about anywhere, including GitHub Pages. Just follow these steps: :::steps #### Enable GitHub Pages action Go to your repo settings and click on the `Pages` tab on the left side, then select `GitHub Actions` as the `Source` ![gh dash](https://cdn.bhvr.dev/CleanShot%202025-06-07%20at%2021.06.59%402x.png) #### Add the GitHub Action File Create a new directory in the root of your project called `.github` and put another folder inside of it called `workflows`. Finally create a file inside of that folder called `deploy-pages.yaml` with the following contents. Pay special attention to the `Build Client` step as that is where you need to replace `your-repo-name` with the actual name of your repo, as well as any environment variables you want included in the build. ```yaml [.github/workflows/deploy-pages.yaml] name: Deploy to GitHub Pages on: # Trigger on pushes to main branch push: branches: [ main ] # Allow manual triggering from Actions tab workflow_dispatch: # Set permissions for GitHub Pages deployment permissions: contents: read pages: write id-token: write # Allow only one concurrent deployment concurrency: group: "pages" cancel-in-progress: false jobs: build: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Bun uses: oven-sh/setup-bun@v1 with: bun-version: latest - name: Install dependencies run: bun install - name: Build shared package run: bun run build:shared - name: Build client run: bun run build:client env: # Set the base URL for GitHub Pages # [!code hl] # Replace 'your-repo-name' with actual values # [!code hl] VITE_BASE_URL: /your-repo-name/ # [!code hl] - name: Setup Pages uses: actions/configure-pages@v4 - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: path: './client/dist' deploy: environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest needs: build steps: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 ``` #### Update the `vite.config.ts` in the `client` package Update the `vite.config.ts` file located in the `client/src` directory to include the `base` property ```typescript [vite.config.ts] import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; import tailwindcss from "@tailwindcss/vite"; import path from "path"; export default defineConfig({ plugins: [react(), tailwindcss()], base: process.env.VITE_BASE_URL || "/", // [!code hl] resolve: { alias: { "@client": path.resolve(__dirname, "./src"), "@server": path.resolve(__dirname, "../server/src"), "@shared": path.resolve(__dirname, "../shared/src"), "@": path.resolve(__dirname, "./src"), }, }, }); ``` #### Push to Deploy! After setting these up you have deployments anytime you push to your main branch! ::: :::tip [Check out the Vite Docs for more info and tips](https://vite.dev/guide/static-deploy#github-pages) ::: ## Netlify One of the classic static site providers still gives developers a great experience for deploying sites, including your bhvr client! There are multiple ways you can deploy your client package to Netlify: * Netlify CLI * Git Provider ### CLI Method The Netlify CLI will help setup a new project on Netlify and create Git webhooks so with every push it will deploy a new version automatically. To enable this for your bhvr project follow these steps. :::steps #### Install `netlify-cli` Install the `netlify-cli` globally so you can use it with your projects ```bash [terminal] bun add -g netlify-cli ``` #### Run Initialization Move into the `client` package and run the initialization commmand to setup a new project ```bash [terminal] cd client ntl init ``` Make sure when setting up the project that the `Directory to Deploy` is `dist` ```bash [terminal] ? Base directory `(blank for current dir): client ? Your build command (hugo build/yarn run build/etc): bun run build ? Directory to deploy (blank for current dir): dist ``` #### Deploy If you setup the Git provider connection offered through the CLI you can just push commits and it will deploy instances. You can also do it through the CLI through the following command in the `client` package: ```bash [terminal] ntl deploy ``` If you want to deploy to production use the `--prod` flag ```bash [terminal] ntl deploy --prod ``` ::: ### Git Provider If you prefer a flow similar to something like Vercel where you get deployments on commits you can use the Git provider flow :::steps #### Start Project From the Projects dashboard click on the `Add new project` button in the top right and select the `Import an existing project` option. ![dashboard](https://cdn.bhvr.dev/netlify-new-project.png) Connect with your Git provider and locate your bhvr project #### Confirm Build Settings After selecting your project make sure the build settings are correct. Generally these are ready out of the box as Netlify will detect what you need. ![build settings](https://cdn.bhvr.dev/netlify-build-settings.png) #### Deploy After confirming the settings Netlify should deploy a new instance with a URL you can visit! ![project overview](https://cdn.bhvr.dev/netlify-project-overview.png) ::: :::tip [Check out the Vite Docs for more info and tips](https://vite.dev/guide/static-deploy#github-pages) ::: ## Cloudflare import { Button } from "vocs/components"; By using Cloudflare Workers you can host the `server` and the `client` in one deployment thanks to the built in [Static Assets Feature](https://hono.dev/docs/getting-started/cloudflare-workers#serve-static-files). With this approach everything will come through a single origin where any routes not covered by Hono will fallback to resolving via the static assets folder, which in our case is our React app. ### Prerequisites This guide assumes you have a bhvr project set up. If not, start here: ```bash bun create bhvr@latest my-app cd my-app ``` ### Configuration :::steps #### 1. Update Your Hono Server Modify `server/src/index.ts` to have a new basepath of `/api` ```typescript import { Hono } from "hono"; import { cors } from "hono/cors"; import type { ApiResponse } from "shared/dist"; const app = new Hono() // [!code --] const app = new Hono().basePath("/api"); // [!code ++] app.use(cors()); app.get("/", (c) => { return c.text("Hello Hono!"); }); app.get("/hello", async (c) => { const data: ApiResponse = { message: "Hello BHVR!", success: true, }; return c.json(data, { status: 200 }); }); export default app; ``` #### 2. Update Your React Client Modify `client/src/App.tsx` to use a dynamic `SERVER_URL` based on dev / prod environment ```typescript import { useState } from "react"; import beaver from "./assets/beaver.svg"; import { ApiResponse } from "shared"; import "./App.css"; const SERVER_URL = import.meta.env.VITE_SERVER_URL || "http://localhost:3000" // [!code --] const SERVER_URL = import.meta.env.DEV ? "http://localhost:3000/api" : "/api"; // [!code ++] function App() { const [data, setData] = useState(); async function sendRequest() { try { const req = await fetch(`${SERVER_URL}/hello`); const res: ApiResponse = await req.json(); setData(res); } catch (error) { console.log(error); } } return ( <>

bhvr

Bun + Hono + Vite + React

A typesafe fullstack monorepo

{data && (
            
              Message: {data.message} 
Success: {data.success.toString()}
)}

Click the beaver to learn more

); } export default App; ``` #### 3. Setup Wrangler Install `wrangler` and it's types at the root of your bhvr project ```bash [terminal] bun add --dev wrangler @cloudflare/workers-types ``` Then create another file at the root of the project called `wrangler.jsonc` with the following content: ```jsonc { "$schema": "./node_modules/wrangler/config-schema.json", "name": "bhvr-project", // Name of your project "main": "./server/dist/index.js", // Path to worker "compatibility_date": "2025-05-25", "assets": { "directory": "./client/dist", // Path to client build folder "not_found_handling": "single-page-application", // Handle SPA routing "run_worker_first": ["/api/*"] }, "compatibility_flags": ["nodejs_compat"] // Enable node for Vite path features } ``` #### 4. Add Deploy Script Append a deploy script to your root `package.json` (alongside the existing bhvr scripts): ```json { "scripts": { "dev": "turbo dev", "dev:client": "turbo dev --filter=client", "dev:server": "turbo dev --filter=server", "build": "turbo build", "build:client": "turbo build --filter=client", "build:server": "turbo build --filter=server", "lint": "turbo lint", "type-check": "turbo type-check", "test": "turbo test", "postinstall": "turbo build --filter=shared --filter=server", "deploy": "turbo build && wrangler deploy --minify" // [!code ++] }, } ``` #### 5. Deploy Make sure you have logged into Cloudflare using Wrangler first ```bash [terminal] bunx wrangler login ``` Then run the deployment script ```bash [terminal] bun run deploy ``` ::: ### Environment Variables You can use environment variables just like you would with Hono + Cloudflare workers as described in the [Hono Docs](https://hono.dev/docs/getting-started/cloudflare-workers#bindings) for Bindings. Here is an example of what you might have in `server/src/index.ts`: ```typescript import { Hono } from "hono"; import { cors } from "hono/cors"; import type { ApiResponse } from "shared/dist"; type Bindings = { SECRET: string; }; const app = new Hono<{ Bindings: Bindings }>().basePath("/api"); app.use(cors()); app.get("/", (c) => { return c.text("Hello Hono!"); }); app.get("/hello", async (c) => { const data: ApiResponse = { message: `Hello BHVR! (this is the secret: ${c.env.SECRET}`, success: true, }; return c.json(data, { status: 200 }); }); export default app; ``` To add the secret in dev you would create a `.dev.vars` file in `server` with the variable ``` SECRET=hotdog ``` :::warning Make sure to add .dev.vars to your `.gitignore`! ::: To add it in production, you can either add it through the Cloudflare dashboard or through Wrangler: ```bash [terminal] bunx wrangler secret put SECRET # Will prompt you to enter the secret ``` For client side variables you can simply include them in a local `.env.local` file in the root of the `client` package, and make sure to use the `VITE_` prefix for them. When you build they will automatically be included in the `dist` bundle. :::warning Make sure only public variables are in the client! ::: ### More Resources ## VPS / Docker import { Button } from "vocs/components"; 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 on a VPS , 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: ```bash bun create bhvr@latest my-app cd my-app ``` ### 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: ```typescript import { Hono } from "hono"; import { cors } from "hono/cors"; import { serveStatic } from "hono/bun"; // [!code ++] 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" })); // [!code ++] app.get("*", async (c, next) => { // [!code ++] return serveStatic({ root: "./static", path: "index.html" })(c, next); // [!code ++] }); // [!code ++] 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: ```typescript import { useState } from "react"; import beaver from "./assets/beaver.svg"; import { ApiResponse } from "shared"; import "./App.css"; function App() { const [data, setData] = useState(); async function sendRequest() { try { // Use relative path - works in both dev and production const req = await fetch("/api/hello"); // [!code hl] const res: ApiResponse = await req.json(); setData(res); } catch (error) { console.log(error); } } return ( <>

bhvr

Bun + Hono + Vite + React

A typesafe fullstack monorepo

{data && (
            
              Message: {data.message} 
Success: {data.success.toString()}
)}

Click the beaver to learn more

); } export default App; ``` #### 3. Configure Vite for Development Update `client/vite.config.ts` to proxy API calls during development: ```typescript 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: { // [!code ++] proxy: { // [!code ++] "/api": { // [!code ++] target: "http://localhost:3000", // [!code ++] changeOrigin: true, // [!code ++] }, // [!code ++] }, // [!code ++] }, // [!code ++] }); ``` #### 4. Add Single Origin Scripts Add these scripts to your root `package.json` (alongside the existing bhvr scripts): ```json { "scripts": { "dev": "turbo dev", "dev:client": "turbo dev --filter=client", "dev:server": "turbo dev --filter=server", "build": "turbo build", "build:client": "turbo build --filter=client", "build:server": "turbo build --filter=server", "build:single": "bun run build && bun run copy:static && bun run build:server", // [!code ++] "copy:static": "rm -rf server/static && cp -r client/dist server/static", // [!code ++] "start:single": "cd server && bun run dist/index.js", // [!code ++] "lint": "turbo lint", "type-check": "turbo type-check", "test": "turbo test", "postinstall": "turbo build --filter=shared --filter=server" } } ``` ### Development vs Production #### Development (Default bhvr) Use the standard bhvr development workflow: ```bash 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: ```bash # 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 ```dockerfile 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 ```bash # Clone your bhvr project git clone 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: ```bash 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: ```typescript // 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