Skip to content
GitHubXDiscordRSS

Bun SPA

Quick guide to initializing and deploying a Bun-based React application to Cloudflare Workers using Alchemy.

This guide shows how to initialize and deploy a Bun-based React TypeScript application to Cloudflare using Alchemy. BunSPA provides a full-stack development experience with Bun’s frontend tooling and Cloudflare Workers for the backend.

You have two options to get started:

Section titled “Option 1: Use Alchemy Create (Recommended)”

Start by creating a new Bun SPA project with Alchemy:

Terminal window
bunx alchemy create my-bun-app --template=bun-spa
cd my-bun-app

Option 2: Add Alchemy to an Existing Bun Project

Section titled “Option 2: Add Alchemy to an Existing Bun Project”

If you’ve already initialized a Bun project with bun init (which supports Tailwind CSS, shadcn/ui, and other templates), you can add Alchemy to it using alchemy init:

Terminal window
# If you haven't already, initialize your Bun project
bun init
# Add Alchemy to the existing project
bun alchemy init --framework bun-spa

The alchemy init command will:

  • Create an alchemy.run.ts file with BunSPA configuration
  • Validate or create bunfig.toml with required env='BUN_PUBLIC_*' configuration
  • Add Alchemy scripts to your package.json (deploy, destroy, alchemy:dev)
  • Install Alchemy as a dev dependency

After initialization, update the paths in alchemy.run.ts to match your project structure:

export const bunsite = await BunSPA("website", {
frontend: "src/index.html", // adjust to match your HTML entrypoint(s)
entrypoint: "src/server.ts", // adjust to match your backend API entrypoint
});

Authenticate once with your Cloudflare account:

Terminal window
bun alchemy login

Run the deploy script generated by the template:

Terminal window
bun run deploy

You’ll get the live URLs of your application:

Terminal window
{
url: "https://website.<your-account>.workers.dev",
apiUrl: "https://website.<your-account>.workers.dev"
}

Work locally using the dev script:

Terminal window
bun run dev

This starts both Bun’s dev server for the frontend (with hot module reloading) and Miniflare for the backend API.

Clean up all Cloudflare resources created by this stack:

Terminal window
bun run destroy

Alchemy requires a locally set password to encrypt Secrets that are stored in state. Be sure to change this.

ALCHEMY_PASSWORD=change-me

alchemy.run.ts is your infrastructure as code with Alchemy. Alchemy commands such as alchemy dev and alchemy deploy use alchemy.run.ts as their entrypoint. Import types from alchemy.run.ts into your application code to get runtime types for Cloudflare Bindings.

BunSPA requires a bunfig.toml file to expose BUN_PUBLIC_* environment variables to the frontend during development:

[serve.static]
env='BUN_PUBLIC_*'

tsconfig.json is created including alchemy.run.ts and registering @cloudflare/workers-types and bun-env.d.ts globally

BunSPA provides a full-stack development experience:

  • Frontend: Bun’s native dev server serves your HTML entrypoints with hot module reloading
  • Backend: Miniflare runs your Cloudflare Worker locally with access to bindings (KV, D1, R2, etc.)
  • Deployment: Both are deployed together to Cloudflare Workers with static assets

You can serve multiple HTML pages by passing an array to the frontend property:

const bunsite = await BunSPA("website", {
frontend: ["src/index.html", "src/about.html"],
entrypoint: "src/worker.ts",
});

Use the getBackendUrl utility to reliably connect to your backend API:

import { getBackendUrl } from "alchemy/cloudflare/bun-spa";
const apiBaseUrl = getBackendUrl();
// Make API requests in your frontent
fetch(new URL('api/request/path', apiBaseUrl))
.then(res => res.json())
.then(data => console.log(data));

This utility automatically uses BUN_PUBLIC_BACKEND_URL in development (injected by Alchemy) and falls back to the current origin in production, allowing your frontend to communicate seamlessly with your backend in both environments.

BunSPA includes Hot Module Replacement (HMR) for instant updates during development. Add this to your main entry file:

src/main.tsx
if (import.meta.hot) {
import.meta.hot.accept();
}

This tells Bun that your module can be hot-replaced, preserving application state when you save changes to your frontend files.