Skip to content

Deployment (Cloudflare)

CruzJS deploys to Cloudflare Pages with automatic provisioning of D1 databases, KV namespaces, R2 buckets, and other bindings. The cruz CLI handles the full deployment lifecycle: infrastructure creation, builds, migrations, and shipping.

If you want to get to production fast:

Terminal window
# 1. Initialize the production environment (creates D1, KV, etc.)
cruz init production
# 2. Deploy (build + migrate + ship)
cruz deploy production

That is the complete workflow. The rest of this page explains each step in detail.

CruzJS supports three environment patterns:

EnvironmentPurposeURL Pattern
productionLive user-facing appmyapp.pages.dev or custom domain
stagingPre-production testingstaging.myapp.pages.dev
previewEphemeral per-branch deploys<branch>.myapp.pages.dev

Each environment gets its own isolated Cloudflare resources (D1 database, KV namespace, etc.), so staging data never touches production.

Before your first deploy to a new environment, run cruz init to provision infrastructure:

Terminal window
cruz init production

This command:

  1. Reads your cruz.config.ts to determine which bindings are needed
  2. Creates a Cloudflare Pages project (if it does not already exist)
  3. Provisions a D1 database named <app-name>-production-db
  4. Creates a KV namespace named <app-name>-production-kv (if kv: true)
  5. Creates an R2 bucket named <app-name>-production-bucket (if r2: true)
  6. Generates the wrangler.toml with all binding IDs
  7. Stores the environment configuration locally

You can initialize multiple environments:

Terminal window
cruz init production
cruz init staging

When you run cruz deploy <env>, the CLI executes these steps in order:

cruz deploy production
├── 1. Build
│ └── Runs `cruz build` (Vite production build for Cloudflare Pages)
├── 2. Migrate
│ └── Runs `cruz db migrate --remote` against the environment's D1 database
├── 3. Ship
│ └── Deploys the built output to Cloudflare Pages via Wrangler
└── 4. Verify
└── Confirms the deployment is live

The build produces a Cloudflare Pages-compatible output with server-side rendering via React Router v7 and the Cloudflare Pages Functions adapter.

Terminal window
# Deploy to production (build + migrate + ship)
cruz deploy production
# Deploy to staging
cruz deploy staging

Deploy a preview from your current branch:

Terminal window
cruz deploy preview

Preview deploys create a unique URL tied to your branch name (e.g., feature-xyz.myapp.pages.dev). They share the staging D1 database by default, or you can configure a dedicated preview database.

View the status of all environments:

Terminal window
cruz status

This shows each environment, its URL, the last deploy time, and the status of all associated resources.

Migrations are applied automatically during cruz deploy, but you can also run them independently:

Terminal window
# Apply migrations to the remote production D1 database
cruz db migrate --remote
# Run a SQL query against the remote database
cruz db query "SELECT count(*) FROM Notes" --remote
  1. Modify your Drizzle schema in *.schema.ts files
  2. Generate a migration: cruz db generate
  3. Review the generated SQL in src/database/migrations/
  4. Test locally: cruz db migrate
  5. Deploy (migrations run automatically): cruz deploy production

If you need to apply migrations without a full deploy:

Terminal window
cruz db migrate --remote --env production

Sensitive values (API keys, auth secrets) are stored as Cloudflare secrets, not in cruz.config.ts or .env files:

Terminal window
# Set a secret
cruz secrets set AUTH_SECRET --env production
# (prompts for the value interactively)
# List all secrets for an environment
cruz secrets list --env production
SecretDescription
AUTH_SECRETSession encryption key (generate with openssl rand -base64 32)
GOOGLE_CLIENT_IDGoogle OAuth client ID (if using Google login)
GOOGLE_CLIENT_SECRETGoogle OAuth client secret
STRIPE_SECRET_KEYStripe API key (if using @cruzjs/pro billing)
STRIPE_WEBHOOK_SECRETStripe webhook signing secret

Set all required secrets before your first production deploy.

A typical setup uses two or three environments:

Terminal window
# Initialize both environments
cruz init staging
cruz init production
# Deploy to staging first
cruz deploy staging
# After testing, deploy to production
cruz deploy production

Define per-environment variables in cruz.config.ts:

import { defineConfig } from '@cruzjs/cli/config';
export default defineConfig({
name: 'myapp',
bindings: { d1: true, kv: true },
vars: {
APP_NAME: 'My App',
},
environments: {
production: {
vars: {
NODE_ENV: 'production',
APP_URL: 'https://myapp.com',
},
domain: 'myapp.com',
},
staging: {
vars: {
NODE_ENV: 'staging',
APP_URL: 'https://staging.myapp.com',
},
},
},
});

Variables in vars (top-level) are shared across all environments. Variables in environments.<name>.vars override the shared values for that specific environment.

Add a custom domain to an environment in cruz.config.ts:

environments: {
production: {
domain: 'myapp.com',
vars: {
APP_URL: 'https://myapp.com',
},
},
},

After deploying, configure DNS to point your domain to Cloudflare Pages. Cloudflare handles SSL certificates automatically.

If you have standalone Workers, Workflows, or Queue consumers in external-processes/, they deploy automatically alongside your main app:

Terminal window
cruz deploy production
# Deploys:
# 1. Main Pages app
# 2. All external-processes/* Workers

Each external process has its own wrangler.toml and deploys independently, but cruz deploy orchestrates all of them together.

Terminal window
# Standalone Worker
cruz new worker email-sender
# Durable Workflow (retryable multi-step process)
cruz new workflow onboarding-flow
# Queue consumer
cruz new queue-worker invoice-processor --queue invoices

To tear down an environment and all its associated Cloudflare resources:

Terminal window
cruz destroy staging

This deletes the D1 database, KV namespace, R2 bucket, and any other resources for that environment. Use with caution — this is irreversible for database data.

For automated deployments in CI, use Wrangler directly with your CLOUDFLARE_API_TOKEN:

Terminal window
# In your CI pipeline
export CLOUDFLARE_API_TOKEN=${{ secrets.CF_API_TOKEN }}
# Build and deploy
cruz build
cruz db migrate --remote --env production
npx wrangler pages deploy ./build/client --project-name myapp

Or use the cruz deploy command if you have the CLI available in CI:

Terminal window
cruz deploy production

Deploy Fails with “D1 database not found”

Section titled “Deploy Fails with “D1 database not found””

Run cruz init <env> to create the database first, then retry the deploy.

Check the migration SQL for syntax incompatible with D1 (which is SQLite-based). Common issues:

  • D1 does not support ALTER COLUMN — use ALTER TABLE ... RENAME COLUMN or create a new table
  • D1 does not support concurrent schema changes — run migrations sequentially

Preview deploys share the staging database by default. To use an isolated database, initialize a dedicated preview environment:

Terminal window
cruz init preview

While Cloudflare is the default deployment target, CruzJS supports deploying to other platforms through runtime adapters. See the Runtime Adapters guide for details on deploying to:

  • AWS — Lambda (serverless) or Fargate (container)
  • Google Cloud — Cloud Run (container) or Cloud Functions (serverless)
  • Azure — Azure Functions (serverless) or Container Apps
  • DigitalOcean — App Platform
  • Docker — Dokploy, Coolify, bare Docker, or Kubernetes

Each adapter maps CruzJS’s abstract bindings (database, cache, queue, storage) to the platform’s native services.

  • Configuration — fine-tune your cruz.config.ts
  • Runtime Adapters — deploy to AWS, GCP, Azure, or Docker
  • Directory Structure — understand the full project layout
  • Set up CI/CD with GitHub Actions for automated staging and production deploys