A small starter for shipping a React app on Cloudflare Workers. It includes:
- React pages rendered by the Worker for
/,/terms, and/privacy - A Worker API under
/api/* - D1 with a sample
userstable and migration runner - R2 with simple private object read/write examples
- A default
workers.devdeploy path, plus a custom domain example
This is meant to be copied into a new project, renamed, and deployed with one Cloudflare API token.
From this repo, copy only the app files:
mkdir -p ../new-project && cp -R wrangler.jsonc .gitignore .env.example vite.config.js package.json worker src scripts migrations ../new-project/Then in the new project:
npm installsrc/ React app, SSR entry, shared route metadata
worker/ Cloudflare Worker backend and API routes
migrations/ D1 SQL migrations
scripts/ Local and remote migration/dev helpers
wrangler.jsonc Worker, assets, D1, R2, and routing config
Run the Vite frontend and local Worker API together:
npm run devOpen http://localhost:5173. Vite proxies /api/* to Wrangler at http://localhost:8787.
For a closer production-style local run where the Worker serves SSR and assets:
npm run workerOpen http://localhost:8787.
These steps work on the Cloudflare free plan. Wrangler can deploy with the API token from .env.
-
Create a Cloudflare account at
https://dash.cloudflare.com. -
Create an API token.
In Cloudflare, open My Profile > API Tokens > Create Token > Custom token.
Add these permissions for the account and zone you are deploying to:
Account > Workers Scripts: EditAccount > Cloudflare Pages: EditAccount > D1: EditAccount > Workers R2 Storage: EditZone > Workers Routes: EditUser > Memberships: ReadUser > User Details: Read
If you will only deploy to
workers.dev,Zone > Workers Routes: Editis not used, but it is needed once you switch to a custom domain. -
Put the token in
.env.cp .env.example .env
Edit
.envso it contains:CLOUDFLARE_API_TOKEN=your-api-token
-
Rename the Worker.
In
wrangler.jsonc, change:to your app name, for example:
"name": "my-app"
-
Create the D1 database.
The starter expects the database name to be
main, which keeps the migration script simple:npx wrangler d1 create main
Copy the
database_idfrom Wrangler's output intowrangler.jsonc, replacing:"database_id": "00000000-0000-0000-0000-000000000000"
-
Create the R2 bucket.
Pick a bucket name for the app and environment. A good pattern is
<app-name>-prod, for examplemy-app-prod.npx wrangler r2 bucket create my-app-prod
Then update
bucket_nameinwrangler.jsonc:"bucket_name": "my-app-prod"
If Cloudflare asks you to enable R2 in the dashboard first, enable it and rerun the command.
-
Apply the remote D1 migration.
npm run migrate:remote
-
Deploy.
npm run deploy
-
Test the deployed app.
If you are using the default
workers.devroute:export APP_URL="https://my-app.<your-workers-subdomain>.workers.dev"
Then:
curl "$APP_URL/api/health" curl "$APP_URL/terms" curl "$APP_URL/privacy"
The starter uses workers_dev by default:
"workers_dev": trueFor a custom domain, remove workers_dev and add routes like this near the end of wrangler.jsonc:
"routes": [
{
"pattern": "example.com/*",
"zone_name": "example.com"
},
{
"pattern": "example.com",
"zone_name": "example.com",
"custom_domain": true
}
],
"placement": {
"mode": "smart"
}Replace example.com with a zone already added to your Cloudflare account.
Local examples after npm run dev or npm run worker:
curl http://localhost:8787/api/health
curl -X POST http://localhost:8787/api/users \
-H "Content-Type: application/json" \
-d '{"email":"person@example.com","name":"Example Person"}'
curl http://localhost:8787/api/users
curl -X PUT http://localhost:8787/api/files/hello.txt \
-H "Content-Type: text/plain" \
--data "Hello from R2"
curl http://localhost:8787/api/files/hello.txtThe migration runner tracks applied SQL files in schema_migrations.
Run local migrations:
npm run migrate:localRun remote migrations:
npm run migrate:remoteThe first migration, migrations/001_init.sql, creates:
users (
id TEXT PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
created_at TEXT NOT NULL
)If you use a database name other than main, either update wrangler.jsonc and run migrations with D1_DATABASE=your-db-name, or pass the database name directly:
npm run migrate:remote -- --db your-db-name- Cloudflare Workers
workers.dev: https://developers.cloudflare.com/workers/configuration/routing/workers-dev/ - Wrangler configuration: https://developers.cloudflare.com/workers/wrangler/configuration/
- D1 migrations: https://developers.cloudflare.com/d1/reference/migrations/
- R2 bucket creation: https://developers.cloudflare.com/r2/buckets/create-buckets/
- API token creation: https://developers.cloudflare.com/fundamentals/api/get-started/create-token/
- API token permissions: https://developers.cloudflare.com/fundamentals/api/reference/permissions/

