Deploy
Kide CMS builds on Astro, so you can deploy it anywhere Astro runs. The setup script provides installation helpers for two targets: Node.js (local SQLite) and Cloudflare Workers (D1 + R2), configuring the database, storage, and image optimization for your choice.
Cloudflare Setup
Section titled “Cloudflare Setup”1. Create the project
Section titled “1. Create the project”pnpx create-kide-app my-site# Select "Cloudflare" when promptedThis generates a wrangler.toml with D1 and R2 bindings.
2. Create Cloudflare resources
Section titled “2. Create Cloudflare resources”cd my-sitepnpm dlx wrangler d1 create my-site-dbpnpm dlx wrangler r2 bucket create my-site-assetsCopy the database_id from the D1 create output into wrangler.toml.
3. Push the database schema
Section titled “3. Push the database schema”pnpm dlx wrangler d1 migrations apply my-site-db --remoteThis applies all pending migrations from src/cms/migrations/ and tracks what’s been applied, so it’s safe to run on every deploy.
4. Deploy
Section titled “4. Deploy”pnpm run deployThis builds the Astro app and deploys it as a Cloudflare Worker.
Running migrations in CI
Section titled “Running migrations in CI”Add the migrations command before the build in your CI/CD or Cloudflare Pages build settings:
pnpm dlx wrangler d1 migrations apply my-site-db && pnpm buildWrangler tracks applied migrations in a d1_migrations table, so only new migrations run. This works automatically in Cloudflare Pages builds since the environment is already authenticated.
Generating new migrations
Section titled “Generating new migrations”When you change your collection schema, generate a new migration:
pnpm db:generateThis regenerates the CMS schema and creates a new SQL migration file in src/cms/migrations/. Commit the migration file — it will be applied on the next deploy.
Wrangler Configuration
Section titled “Wrangler Configuration”The generated wrangler.toml:
name = "my-site"compatibility_date = "2025-01-01"compatibility_flags = ["nodejs_compat"]
[[d1_databases]]binding = "CMS_DB"database_name = "my-site-db"database_id = "" # from wrangler d1 createmigrations_dir = "src/cms/migrations"
[triggers]crons = ["* * * * *"] # scheduled publishing
[[r2_buckets]]binding = "CMS_ASSETS"bucket_name = "my-site-assets"Storage
Section titled “Storage”On Cloudflare, assets are stored in R2 instead of the local filesystem. The CMS uses a storage abstraction (src/cms/adapters/storage.ts) that the setup script configures:
- Local/Node.js: Files go to
public/uploads/, served by Astro’s static file handling - Cloudflare: Files go to R2 via the
CMS_ASSETSbinding, served by a dynamic route at/uploads/[...path].ts
No code changes needed. The setup script copies the correct storage.ts for your target.
Database
Section titled “Database”- Local/Node.js: SQLite via
better-sqlite3, stored atdata/cms.db - Cloudflare: D1 (Cloudflare’s distributed SQLite), accessed via the
CMS_DBbinding
The setup script copies the correct db.ts for your target, similar to storage.
Scheduled Publishing
Section titled “Scheduled Publishing”Kide CMS supports scheduled publish/unpublish for content. The approach differs by target:
Cloudflare
Section titled “Cloudflare”A cron trigger fires every minute. The CMS integration injects a scheduled handler into the built worker entry that calls /api/cms/cron/publish internally.
Set the CRON_SECRET env var on your worker to secure the endpoint:
pnpm dlx wrangler secret put CRON_SECRETNode.js
Section titled “Node.js”Set up an external cron job to call the publish endpoint:
# crontab* * * * * curl -s -H "Authorization: Bearer YOUR_SECRET" http://localhost:4321/api/cms/cron/publishSet CRON_SECRET in your .env to match.
In dev mode, the middleware handles scheduled publishing on every request (no external cron needed).
Image Optimization
Section titled “Image Optimization”- Local/Node.js: Sharp-based on-demand transformation via
/api/cms/img/. Images are resized, converted to WebP, and cached in.cms-cache/. The admin UI uses thumbnails automatically. - Cloudflare: Cloudflare Image Transformations via
/cdn-cgi/image/. Sharp is not used.
The cmsImage() and cmsSrcset() helpers detect the runtime and generate the correct URLs automatically. The admin UI uses 480px thumbnails for asset grids and image pickers.
Cloudflare Image Transformations
Section titled “Cloudflare Image Transformations”Cloudflare Image Transformations must be enabled on your zone for image resizing to work in production:
- Go to your Cloudflare dashboard → Images → Transformations
- Enable Resize images from any origin
Once enabled, URLs like /cdn-cgi/image/width=480,format=webp,quality=80/uploads/photo.jpg are handled automatically by Cloudflare’s edge network.
In local development with pnpm dev, images are served from R2 without transformation (wrangler doesn’t emulate /cdn-cgi/image/). This is fine for development — transformations only apply in production.
If Image Transformations are not available on your Cloudflare plan, images will be served at full size. The admin UI and public site will still work — just without resizing.
Environment Variables
Section titled “Environment Variables”| Variable | Required | Description |
|---|---|---|
CRON_SECRET | Recommended | Secures the scheduled publishing endpoint |
RESEND_API_KEY | Optional | Enables automatic invite emails via Resend |
RESEND_FROM_EMAIL | Optional | Email sender address |
AI_PROVIDER | Optional | AI provider for content generation |
AI_API_KEY | Optional | AI provider API key |
AI_MODEL | Optional | AI model name |
Setting variables
Section titled “Setting variables”Cloudflare — set secrets with Wrangler and non-secret values in wrangler.toml:
pnpm dlx wrangler secret put CRON_SECRET[vars]CF_BEACON_TOKEN = "your-token"Node.js — add variables to a .env file in the project root:
CRON_SECRET=your-secretRESEND_API_KEY=re_xxxReading variables in code
Section titled “Reading variables in code”On Cloudflare Workers, runtime secrets are not available via import.meta.env. Astro only exposes build-time and PUBLIC_-prefixed variables there. Instead, use the env export from cloudflare:workers:
import { env as cfEnv } from "cloudflare:workers";
const env = (key: string) => (cfEnv as Record<string, string>)[key] ?? import.meta.env[key];The cloudflare:workers import works both locally (via wrangler emulation during pnpm dev) and on deployed Workers. The import.meta.env fallback is a safety net but not strictly needed since the Cloudflare runtime always provides the bindings.
Using import.meta.env alone will silently return undefined for secrets on Cloudflare, causing features like AI and email to appear disabled even when the variables are set.
Local Development
Section titled “Local Development”For Cloudflare projects, pnpm dev uses Astro’s dev server with local D1/R2 emulation via miniflare. Your wrangler.toml bindings work locally without any extra setup.
Node.js Deployment
Section titled “Node.js Deployment”For Node.js targets, the build output is a standalone server:
pnpm buildnode dist/server/entry.mjsNo extra configuration needed. SQLite database and uploads are stored locally.