Skip to content

Step 5: Deploy to Cloudflare

import { Steps, Aside } from ‘@astrojs/starlight/components’;

You have a working to-do app locally. Now deploy it to Cloudflare Pages with a real D1 database in a few commands.

You need:

  • A Cloudflare account (free at cloudflare.com)
  • The Wrangler CLI authenticated: npx wrangler login

If you haven’t logged in before:

Terminal window
npx wrangler login
# Opens a browser — authorize the CLI to your Cloudflare account

Step 1: Initialize the Production Environment

Section titled “Step 1: Initialize the Production Environment”

cruz init reads your cruz.config.ts, creates all the Cloudflare resources your app needs, and generates the wrangler.toml:

Terminal window
cruz init production

This single command:

  1. Creates a Cloudflare Pages project named my-todos
  2. Provisions a D1 database named my-todos-production-db
  3. Creates a KV namespace for session storage
  4. Generates wrangler.toml with all binding IDs wired up

You will see output like:

✓ Cloudflare Pages project created: my-todos
✓ D1 database created: my-todos-production-db (id: abc123...)
✓ KV namespace created: my-todos-production-kv (id: def456...)
✓ wrangler.toml updated

Secrets are sensitive values (auth keys, API tokens) stored in Cloudflare’s secret store — not in your repository or config files.

Terminal window
# Generate a strong session secret
openssl rand -base64 32
# Set it in Cloudflare (paste the value when prompted)
cruz secrets set AUTH_SECRET --env production

If you’re using email (Resend, Postmark, etc.):

Terminal window
cruz secrets set RESEND_API_KEY --env production
SecretRequiredDescription
AUTH_SECRETYesSession signing key — openssl rand -base64 32
RESEND_API_KEYIf sending emailEmail delivery API key
GOOGLE_CLIENT_IDIf Google loginOAuth client ID
GOOGLE_CLIENT_SECRETIf Google loginOAuth client secret
Terminal window
cruz deploy production

This runs three steps in order:

cruz deploy production
├── 1. Build
│ └── Vite production build (Cloudflare Pages output)
├── 2. Migrate
│ └── Applies pending migrations to your remote D1 database
│ (the Todo table migration from Step 2 runs here)
└── 3. Ship
└── Deploys to Cloudflare Pages via Wrangler

The first deploy takes about 60–90 seconds. Subsequent deploys are faster because Wrangler only uploads changed files.

When it finishes, you will see:

✓ Build complete
✓ Migrations applied (1 new migration)
✓ Deployed to: https://my-todos.pages.dev

Open your app at https://my-todos.pages.dev.

Visit your production URL and:

  1. Sign up for a new account
  2. Create a todo
  3. Mark it complete
  4. Delete it

Everything should work identically to your local dev environment.

To use your own domain instead of my-todos.pages.dev:

  1. Update cruz.config.ts:
environments: {
production: {
domain: 'todos.mysite.com',
vars: {
APP_URL: 'https://todos.mysite.com',
},
},
},
  1. Re-deploy:
Terminal window
cruz deploy production
  1. In your DNS provider, add a CNAME record pointing todos.mysite.com to my-todos.pages.dev.

Cloudflare handles SSL certificates automatically. Your app will be available on your custom domain within minutes of DNS propagation.

After making changes to your app, deploy again:

Terminal window
# Make changes, then:
cruz deploy production

If you added a new table or column, cruz db generate (run locally) creates the migration file, and cruz deploy applies it to production automatically.

It is good practice to have a staging environment to test changes before production:

Terminal window
# One-time setup
cruz init staging
# Deploy to staging for testing
cruz deploy staging
# After verifying on staging, deploy to production
cruz deploy production

Staging and production each have their own isolated D1 database, so staging data never touches production.

To deploy automatically on every push to main, add a GitHub Actions workflow:

.github/workflows/deploy.yml
name: Deploy to Production
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm run build
- run: npx wrangler pages deploy ./build/client --project-name my-todos
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
- run: npx wrangler d1 migrations apply my-todos-production-db --remote
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}

Add CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID as repository secrets in GitHub Settings → Secrets and Variables → Actions.

Get your API token from Cloudflare Dashboard → My Profile → API Tokens — create one with Cloudflare Pages: Edit and D1: Edit permissions.

WhereWhat
Cloudflare PagesReact Router SSR + tRPC handler (runs at the edge, globally)
D1 databaseYour SQLite data (replicated globally by Cloudflare)
KV namespaceSession storage (globally distributed)
Your browserReact client-side hydration + tRPC hooks

Every request is handled by a Cloudflare Worker in a data center near the user — typically under 50ms cold start globally.


Congratulations — you’ve built and deployed a full-stack app with CruzJS.

Continue the tutorial to add more features:

Or explore the rest of the framework:

  • CRUD Factory — replace the manual service + router with createCrud() for standard features
  • CLI Scaffolding — generate new features with cruz new feature