Deploy Server
Deploy the Hono server to Cloudflare Workers with D1, R2, and Secrets
Deploy Server (Cloudflare Workers)
EasyStarter's server is built with Hono and runs on Cloudflare Workers, using D1 as the database and R2 as object storage.
Before starting, confirm that the following prerequisites are in place:
- Cloudflare credentials ready (see Cloudflare Integration)
- D1 database created and Database ID on hand (see Database)
- R2 bucket created and bucket name on hand (see Storage)
EasyStarter supports two deployment methods — choose the one that fits your workflow:
| Method | Best for |
|---|---|
| Option 1: Local CLI | Quick launch, one-off deploys, full manual control |
| Option 2: GitHub auto-deploy | Continuous delivery, team collaboration, deploy on push |
Option 1: Local CLI deploy
Authenticate Wrangler locally, then run the deploy commands manually.
npx wrangler loginEnvironment variable overview
Server variables live in three separate places:
| Type | Location | Description |
|---|---|---|
| Public config | apps/server/wrangler.jsonc → vars | Non-sensitive values — stored in plain text, deployed with code |
| Local dev | apps/server/.dev.vars | Auto-loaded by wrangler dev, never deployed |
| Production secrets | apps/server/.env.production | Pushed to Workers Secrets via wrangler secret bulk, never in build output |
Never commit
.env.productionto Git. Add.dev.varsto.gitignoreas well.
Update apps/server/wrangler.jsonc
Fill in your Worker name, D1 Database ID, R2 bucket name, and public variables:
{
"name": "your-server-worker", // Worker name — globally unique, determines default URL
"main": "src/index.ts",
"compatibility_date": "2025-06-15",
"compatibility_flags": ["nodejs_compat"],
"d1_databases": [
{
"binding": "DB",
"database_name": "easystarter-db",
"database_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" // D1 Database ID (UUID)
}
],
"vars": {
"NODE_ENV": "production",
"WEBSITE_URL": "https://your-app.com", // Public URL of the web app
"SERVER_URL": "https://your-server.workers.dev", // Public URL of this server Worker
"GITHUB_CLIENT_ID": "your-github-client-id", // Public — safe to put in vars
"GOOGLE_CLIENT_ID": "your-google-client-id" // Public — safe to put in vars
},
"r2_buckets": [
{
"binding": "STORAGE",
"bucket_name": "easystarter-bucket" // R2 bucket name
}
]
}| Field | Description |
|---|---|
name | Worker name — default URL is https://<name>.<subdomain>.workers.dev |
database_id | UUID of the D1 database |
vars.WEBSITE_URL | Web app URL — used by Better Auth for callback URLs and email links |
vars.SERVER_URL | This server Worker's URL — used by Better Auth config and CORS |
bucket_name | Must match the R2 bucket name in the Cloudflare Dashboard |
Prepare production secrets (.env.production)
Create apps/server/.env.production with all sensitive variables. This file is never included in the build — it is only used by wrangler secret bulk in the next step.
# Better Auth — signs sessions; must be a long random string
BETTER_AUTH_SECRET=
# GitHub OAuth — Secret only; Client ID is already in wrangler.jsonc vars
GITHUB_CLIENT_SECRET=
# Google OAuth — Secret only; Client ID is already in wrangler.jsonc vars
GOOGLE_CLIENT_SECRET=
# Resend email service
RESEND_API_KEY=
# R2 public access URL (from R2.dev subdomain or custom domain)
R2_PUBLIC_URL=
# Stripe payments
STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=
# RevenueCat (only needed when mobile in-app purchases are enabled)
REVENUECAT_SECRET_API_KEY=Key notes:
- Generate
BETTER_AUTH_SECRETwithopenssl rand -base64 32— minimum 32 characters - Only the Secret keys for GitHub and Google go here; their Client IDs are already in
wrangler.jsoncvarsas public values R2_PUBLIC_URLis the public-facing URL of your R2 bucket — enable the R2.dev subdomain or bind a custom domain in the R2 bucket settings to get it- Variables for unused integrations (e.g. RevenueCat) can be left empty or removed
Deploy the Worker
pnpm deploy:serverThis compiles apps/server/src/index.ts and publishes it to Cloudflare Workers. On success:
Deployed your-server-worker triggers:
https://your-server-worker.your-subdomain.workers.devSave this URL — you'll need it when configuring the web app and updating SERVER_URL.
Push secrets
Encrypt and store all variables from .env.production in Workers Secrets:
pnpm -F server secrets:bulk:productionThis runs wrangler secret bulk .env.production. Each value is encrypted at rest on Cloudflare's side and never appears in deployed code or logs.
Secrets and code deployments are independent. To update a sensitive variable, re-run this command — no redeploy needed.
Run database migrations
Apply the database schema to Cloudflare D1. The pnpm db:migrate command uses drizzle-kit's D1 HTTP driver, which requires these three values in apps/server/.dev.vars:
CLOUDFLARE_ACCOUNT_ID= # Your Cloudflare account ID
CLOUDFLARE_API_TOKEN= # API token with D1 Edit permission
CLOUDFLARE_D1_DATABASE_ID= # D1 database UUIDThen run:
pnpm db:migrateThis creates all required tables in the production D1 database (users, sessions, subscriptions, billing, etc.).
After every schema change: run
pnpm db:generateto produce a new migration file, thenpnpm db:migrateto apply it to production.
Verify the deployment
In the Cloudflare Dashboard, go to Workers & Pages → select your Worker → Logs to view live request logs and confirm the service is responding correctly.
Option 2: GitHub auto-deploy
Connect your GitHub repository to Cloudflare so every push to the target branch automatically triggers a build and deploy — no local commands needed.
Connect your GitHub repository
- Go to Cloudflare Dashboard → Workers & Pages
- Click Create → Workers → Connect to Git
- Authorize Cloudflare to access your GitHub account and select your repository
- Choose the deployment branch (usually
main)
Push secrets
After connecting the repository but before the first build fires, push all secrets to Cloudflare from your local machine so the Worker has everything it needs on startup:
pnpm -F server secrets:bulk:productionThis runs wrangler secret bulk .env.production, encrypting every variable in apps/server/.env.production into Worker Secrets.
Secrets and code deploys are independent. You only need to re-push when a secret value changes — not on every code update.
Enter the build configuration
Fill in the following settings on the build configuration page:
| Field | Value |
|---|---|
| Root directory | / |
| Build command | pnpm --filter server build |
| Deploy command | pnpm --filter server run deploy |
| Version command | pnpm --filter server run deploy |
Root directory is
/because this is a monorepo — pnpm workspaces must resolve dependencies from the repo root.
Run database migrations
Auto-deploy does not run database migrations automatically. After the first deploy, run this manually from your local machine:
pnpm db:migrateMake sure apps/server/.dev.vars has these values filled in:
CLOUDFLARE_ACCOUNT_ID=
CLOUDFLARE_API_TOKEN=
CLOUDFLARE_D1_DATABASE_ID=For every future schema change: run
pnpm db:generatelocally to generate a migration file, thenpnpm db:migrateto apply it to the production D1.
Once configured, every push to the target branch automatically triggers a new build and deployment. View the status and logs for each deploy under Workers & Pages → your Worker → Deployments.
Custom domain (Recommended)
- Go to Workers & Pages → select your server Worker → Settings → Domains & Routes
- Click Add Custom Domain and enter a domain hosted on Cloudflare (e.g.
api.yourdomain.com) - Once the domain is bound, update the related config as described below
Why these two fields must be updated
SERVER_URL and WEBSITE_URL are not ordinary environment variables — they live in the vars block of wrangler.jsonc and are compiled into the Worker bundle at deploy time. Changing them requires a redeploy to take effect.
These two fields are critical for the authentication system:
| Field | Used for |
|---|---|
SERVER_URL | Better Auth baseURL; OAuth callback URLs (/api/auth/callback/github, etc.); cookie domain and secure policy |
WEBSITE_URL | Better Auth trustedOrigins (CORS allowlist); redirect links in transactional emails |
If either value doesn't match the actual domain, OAuth callbacks will return 404, cross-origin requests will be blocked by CORS, and session cookies won't be set.
Files to update
① apps/server/wrangler.jsonc
"vars": {
"WEBSITE_URL": "https://your-app.com", // final domain of the web app
"SERVER_URL": "https://api.yourdomain.com" // the custom domain you just bound
}② apps/web/wrangler.jsonc
The web app also holds the server address for direct API calls from the frontend:
"vars": {
"VITE_SERVER_URL": "https://api.yourdomain.com" // must match SERVER_URL above
}③ OAuth app settings
If you changed SERVER_URL, update the callback URLs in each OAuth provider:
- GitHub: Settings → Developer settings → OAuth Apps → update Authorization callback URL to
https://api.yourdomain.com/api/auth/callback/github - Google: Google Cloud Console → Credentials → OAuth 2.0 Client → update Authorized redirect URIs
Redeploy to apply the changes
Both apps have changes, so deploy both:
pnpm deploy:server
pnpm deploy:web
varsare static config compiled into the bundle — they are not Secrets. Any change tovarsinwrangler.jsoncrequires a redeploy. Runningwrangler secret bulkalone will not update these values.