Skip to content

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.

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:

Terminal window
pnpm dlx wrangler d1 create my-site-db # paste the id into wrangler.toml
pnpm dlx wrangler r2 bucket create my-site-assets
pnpm dlx wrangler d1 migrations apply my-site-db --remote
pnpm run deploy

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

Terminal window
pnpm dlx wrangler d1 migrations apply my-site-db --remote && pnpm build

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

Terminal window
pnpm dlx wrangler secret put CRON_SECRET

Images. Enable Images → Transformations → Resize images from any origin in the Cloudflare dashboard. Without it, images serve at full size. Everything else still works.

Terminal window
pnpm build
pnpm cms:push # sync schema — fails loudly, stopping a broken deploy
node dist/server/entry.mjs

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

Terminal window
CMS_DATABASE_URL=/srv/kide/shared/cms.db # default: ./data/cms.db
CMS_UPLOADS_DIR=/srv/kide/shared/uploads # default: ./public/uploads

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

server.mjs
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:

Terminal window
* * * * * 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/tasks
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.