Deploy
Kide builds on Astro and deploys anywhere Astro runs. The scaffolder wires one of two targets: Node.js (SQLite + local files) or Cloudflare Workers (D1 + R2). Both distribution modes (embedded or package) deploy identically.
| Node.js | Cloudflare | |
|---|---|---|
| Database | SQLite at data/cms.db |
D1 (CMS_DB binding) |
| Assets | public/uploads/ |
R2 (CMS_ASSETS binding) |
| Images | Sharp, on-demand + cached | Cloudflare Image Transformations |
Two one-line adapter files handle the difference: src/cms/adapters/db.ts and storage.ts. The scaffolder already sets them.
Cloudflare
Section titled “Cloudflare”create-kide-app offers to provision everything during scaffolding: D1 database, R2 bucket, migrations, and the first deploy. If you accepted, you’re already live. This section covers the manual path and day-2 operations.
Skipped provisioning? Four commands:
pnpm dlx wrangler d1 create my-site-db # paste the id into wrangler.tomlpnpm dlx wrangler r2 bucket create my-site-assetspnpm dlx wrangler d1 migrations apply my-site-db --remotepnpm run deploySchema changes. After editing collections, run pnpm db:generate and commit the migration file it writes to src/cms/migrations/. In CI, apply migrations before the build:
pnpm dlx wrangler d1 migrations apply my-site-db --remote && pnpm buildThe --remote flag matters: without it, wrangler migrates your local D1 and production stays on the old schema.
Scheduled publishing works without setup. A cron trigger fires every minute and the worker handles publishing and background tasks. Secure the endpoints:
pnpm dlx wrangler secret put CRON_SECRETImages. Enable Images → Transformations → Resize images from any origin in the Cloudflare dashboard. Without it, images serve at full size. Everything else still works.
Node.js
Section titled “Node.js”pnpm buildpnpm cms:push # sync schema — fails loudly, stopping a broken deploynode dist/server/entry.mjsThe server does no schema work at boot, so cms:push belongs in every deploy. Additive changes apply directly. An ambiguous rename/drop is refused with guidance: run pnpm cms:push --recreate=<slugs> --allow-data-loss to drop and recreate the named tables (plus their _translations/_versions tables — data loss, fine for a DB you’re repopulating), or hand-write a migration to preserve data.
Persistent storage. If deploys replace the app directory (containers, fresh releases), point these at a location outside it, and back that one place up:
CMS_DATABASE_URL=/srv/kide/shared/cms.db # default: ./data/cms.dbCMS_UPLOADS_DIR=/srv/kide/shared/uploads # default: ./public/uploadsHave your web server (Caddy, nginx) serve /uploads/* straight from CMS_UPLOADS_DIR.
Compression. The default node({ mode: "standalone" }) adapter serves assets uncompressed — fine behind a CDN or reverse proxy that compresses. On a bare VM with nothing in front, switch to node({ mode: "middleware" }) in astro.config.mjs and wrap the handler in a small Express server:
import express from "express";import compression from "compression";import { handler } from "./dist/server/entry.mjs";
const app = express();app.use(compression());app.use("/_astro", express.static("dist/client/_astro", { immutable: true, maxAge: "1y" }));app.use(express.static("dist/client"));app.use(handler);app.listen(4321);Scheduled publishing. Nothing polls the cron endpoints on Node — that’s the Cloudflare Worker’s scheduled() handler. Self-hosted deployments must poll both endpoints once a minute or scheduled publishing and background tasks never run:
* * * * * curl -s -H "Authorization: Bearer $CRON_SECRET" https://example.com/api/cms/cron/publish* * * * * curl -s -H "Authorization: Bearer $CRON_SECRET" https://example.com/api/cms/cron/tasksEnvironment variables
Section titled “Environment variables”| Variable | Required | Description |
|---|---|---|
CRON_SECRET |
Required | Secures the cron endpoints (401 in production until set) |
CMS_TRUSTED_ORIGIN |
Recommended | Canonical public origin used by admin CSRF checks, e.g. https://cms.example.com |
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 |
WEBHOOK_SECRET_<PROVIDER> |
Optional | HMAC secret for inbound webhooks at /api/cms/webhooks/<provider> (hyphens in the provider name map to underscores) |
Node.js: put them in .env. Cloudflare: secrets via pnpm dlx wrangler secret put <NAME>, non-secret values under [vars] in wrangler.toml.