EasyStarter logoEasyStarter

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:

EasyStarter supports two deployment methods — choose the one that fits your workflow:

MethodBest for
Option 1: Local CLIQuick launch, one-off deploys, full manual control
Option 2: GitHub auto-deployContinuous delivery, team collaboration, deploy on push

Option 1: Local CLI deploy

Authenticate Wrangler locally, then run the deploy commands manually.

npx wrangler login

Environment variable overview

Server variables live in three separate places:

TypeLocationDescription
Public configapps/server/wrangler.jsoncvarsNon-sensitive values — stored in plain text, deployed with code
Local devapps/server/.dev.varsAuto-loaded by wrangler dev, never deployed
Production secretsapps/server/.env.productionPushed to Workers Secrets via wrangler secret bulk, never in build output

Never commit .env.production to Git. Add .dev.vars to .gitignore as well.

Update apps/server/wrangler.jsonc

Fill in your Worker name, D1 Database ID, R2 bucket name, and public variables:

apps/server/wrangler.jsonc
{
  "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
    }
  ]
}
FieldDescription
nameWorker name — default URL is https://<name>.<subdomain>.workers.dev
database_idUUID of the D1 database
vars.WEBSITE_URLWeb app URL — used by Better Auth for callback URLs and email links
vars.SERVER_URLThis server Worker's URL — used by Better Auth config and CORS
bucket_nameMust 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.

apps/server/.env.production
# 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_SECRET with openssl rand -base64 32 — minimum 32 characters
  • Only the Secret keys for GitHub and Google go here; their Client IDs are already in wrangler.jsonc vars as public values
  • R2_PUBLIC_URL is 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:server

This 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.dev

Save 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:production

This 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:

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 UUID

Then run:

pnpm db:migrate

This creates all required tables in the production D1 database (users, sessions, subscriptions, billing, etc.).

After every schema change: run pnpm db:generate to produce a new migration file, then pnpm db:migrate to 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

  1. Go to Cloudflare DashboardWorkers & Pages
  2. Click CreateWorkersConnect to Git
  3. Authorize Cloudflare to access your GitHub account and select your repository
  4. 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:production

This 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:

FieldValue
Root directory/
Build commandpnpm --filter server build
Deploy commandpnpm --filter server run deploy
Version commandpnpm --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:migrate

Make 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:generate locally to generate a migration file, then pnpm db:migrate to 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.


  1. Go to Workers & Pages → select your server Worker → Settings → Domains & Routes
  2. Click Add Custom Domain and enter a domain hosted on Cloudflare (e.g. api.yourdomain.com)
  3. 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:

FieldUsed for
SERVER_URLBetter Auth baseURL; OAuth callback URLs (/api/auth/callback/github, etc.); cookie domain and secure policy
WEBSITE_URLBetter 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

vars are static config compiled into the bundle — they are not Secrets. Any change to vars in wrangler.jsonc requires a redeploy. Running wrangler secret bulk alone will not update these values.