Deployment
Alokai CMS ships two workers per environment — cms (the dashboard +
API) and alokai-cms-mcp (the AI tooling worker that the dashboard binds
to). Both must be deployed in lockstep: MCP first, CMS second. We also
apply pending D1 migrations against the target environment as part of
each deploy.
There are two deployment paths:
- GitHub Actions (preferred) —
ci.ymlruns build/test/lint on every PR and push tomain, and on push-to-mainit chains adeploy-stagingjob that deploys MCP + CMS to staging once tests pass.deploy-prod.ymlis a manualworkflow_dispatchflow gated by theproductionGitHub Environment’s required reviewer. - Manual local deploy (break-glass) — run
yarn workspace cms deploy:staging/deploy:prodplus the MCP equivalents from a developer machine. Use this only when GitHub Actions is unavailable.
Prerequisites
Before either path will work, make sure:
- Cloudflare resources exist for the target environment (D1, R2, KV, Durable Objects, MCP worker).
- All required secrets are set on the target Worker.
- For GitHub Actions: the
stagingandproductionEnvironments exist in repo settings, each holdingCLOUDFLARE_API_TOKEN,CLOUDFLARE_ACCOUNT_ID, andVERDACCIO_AUTH_TOKEN.
Pre-deploy checklist
Run before triggering prod:
- CI on
mainis green (build + test + lint). - Staging is at the same commit as
mainand the staging dashboard loads, login works, and a sample page renders. - If new D1 migrations exist,
yarn workspace cms db:migrate:list:stagingshows them as already applied (the staging deploy applies first). - No outstanding
wrangler.jsoncschema changes need a backfill. - You know which version bump (
patch/minor/major) you’ll pick for the prod deploy.
Stage deploy (automated)
ci.yml runs two jobs in sequence:
-
build-test-lint— runs on every PR and every push tomain.- Installs (
yarn install --immutable) with the enterprise registry token. - Builds
cms,@alokai/cms-api,@alokai/cms-mcpvia Turbo. - Runs
yarn turbo run test --filter=cmsandyarn turbo run lint. - On a PR, the workflow stops here.
- Installs (
-
deploy-staging— only runs on push tomain(or a manualworkflow_dispatch), and only afterbuild-test-lintsucceeds.- Auto-backfills
d1_migrationsif the staging DB has the schema applied but the wrangler migration tracker is empty (legacy runtime-migrated DB). See Migrations below. - Applies pending D1 migrations with
wrangler d1 migrations apply alokai-cms-db-staging --remote --env staging. - Deploys MCP (
alokai-cms-mcp-staging) so the CMS service binding resolves. Stamps a deploy message:GitHub Actions deploy by <actor> @ <sha> (<run-url>). - Deploys CMS (
cms-staging) with the same message. - Smoke checks
https://cms-staging.alokai-apps.com/api/health(5 retries, 5s apart). Job fails loudly if it never returns 200.
- Auto-backfills
To rerun without a code change, click Run workflow on the CI
action page in GitHub — workflow_dispatch re-triggers both jobs.
The deploy message lands in the Cloudflare dashboard’s Workers &
Pages → cms-staging → Deployments list and in
wrangler deployments list --env staging, so you can map any prod
deployment back to the GitHub Actions run that produced it.
Production deploy (manual, gated)
deploy-prod.yml is workflow_dispatch only. To trigger it:
- Open Actions → Deploy to Production → Run workflow.
- Pick
version_bump:patch(default),minor, ormajor. - Click Run workflow. The job pauses until a member of the
productionGitHub Environment’s required-reviewers list approves. - After approval the workflow:
- Runs
yarn turbo run test --filter=cmsas a final gate. - Bumps
apps/cms/package.jsonvia a direct JSON rewrite (the sidebar badge and/api/healthread this — it’s how we tell stale bundles from fresh). We don’t usenpm versionbecause npm 10+ in workspace mode does a peer-deps re-resolution that conflicts with@storefront-ui/react’s React-18 peer. - Builds, auto-backfills
d1_migrationsif needed (see Migrations), applies pending D1 migrations to prod, deploys MCP, deploys CMS — each deploy stamped withcms@X.Y.Z prod deploy by <actor> @ <sha> (<run-url>). - Smoke-checks
https://cms.alokai-apps.com/api/healthand asserts the deployedversionfield matches the bumped value. - Commits the version bump, tags
cms-vX.Y.Z, pushes, and creates a GitHub Release with auto-generated notes.
- Runs
If the smoke check fails, the workflow stops after the deploy itself completed but before the tag/release commit. See Rollback for what to do.
Manual local deploy (break-glass)
When GitHub Actions can’t run (outage, fork, ad-hoc hotfix from a laptop):
# Make sure you're authenticated to the right Cloudflare account.npx wrangler whoami
# Build everything first.yarn build:cms
# Apply pending migrations to the target environment.yarn workspace cms db:migrate:staging # or db:migrate:prod
# Deploy MCP first, then CMS.yarn workspace @alokai/cms-mcp deploy:staging # or deploy:prodyarn workspace cms deploy:staging # or deploy:prod
# Smoke-check the new deployment.curl -fsS https://cms-staging.alokai-apps.com/api/healthFor prod, also run yarn workspace cms prepare:deployment:patch (or
:minor / :major) before the build step so the version bump is
recorded; otherwise the sidebar badge / /api/health won’t reflect the
shipped change.
Post-deploy verification
After every deploy:
curl https://<env-host>/api/health→ expect200and the version you just shipped.- Open the dashboard, log in, and load a sample page in the Page Builder.
- Spot-check the published delivery API on a known page slug.
- Watch
wrangler tail --env <env>for ~1 minute for unexpected errors.
Rollback
The CMS uses Cloudflare Workers’ built-in deployment history.
# List recent deployments for the target worker.npx wrangler deployments list --env staging # omit --env for prod
# Roll back to the previous deployment.npx wrangler rollback --message "rolling back: <reason>" --env stagingRepeat for the MCP worker if its rollback is needed:
cd packages/cms-mcpnpx wrangler rollback --message "rolling back MCP: <reason>" --env stagingMigrations
Schema lives in apps/cms/migrations/*.sql, applied by wrangler d1 migrations apply. The wrangler d1_migrations table on each D1
instance records which files have been applied.
Why we have an auto-backfill step. The CMS originally used a
runtime self-heal in apps/cms/src/server/migrate.ts that materialized
the schema on first request. Production and staging DBs that pre-date
the wrangler-tracked convention have the full schema but an empty
d1_migrations table — so a naive wrangler d1 migrations apply
re-runs all 35 historical files and trips “duplicate column” on the
first ALTER TABLE ADD COLUMN (SQLite has no IF NOT EXISTS for ADD
COLUMN, so individual migration files cannot be made idempotent in
pure SQL).
The deploy workflows handle this automatically:
# Pseudocode of the step:if "organizations" exists AND "d1_migrations" does not exist: wrangler d1 execute --file=scripts/backfill-d1-migrations.sqlelse: skip # tracker is in sync, or DB is freshapps/cms/scripts/backfill-d1-migrations.sql does
INSERT OR IGNORE INTO d1_migrations (name) VALUES ('001_initial.sql'), ...
for all 35 file names. Idempotent on every dimension — both the
detection and the SQL.
Adding a new migration:
- Create the next file in order:
apps/cms/migrations/036_my_change.sql. - Use idempotent statements where SQLite supports it
(
CREATE TABLE IF NOT EXISTS,CREATE INDEX IF NOT EXISTS,INSERT OR IGNORE). ForALTER ADD COLUMNand other non-idempotent DDL, thed1_migrationstracker is the safety net. - Add the filename to
apps/cms/scripts/backfill-d1-migrations.sql’s list only if existing prod DBs already have the corresponding schema applied via runtime self-heal. New columns added post-cutover don’t need backfill entries. - Apply locally with
yarn workspace cms db:migrate.
Manual backfill (if you ever need to run it by hand for a fresh environment standup):
yarn workspace cms db:migrate:backfill:staging # or :prodyarn workspace cms db:migrate:list:staging # confirm 35 ✅yarn workspace cms db:migrate:staging # "No migrations to apply!"What gets deployed
- CMS Worker —
apps/cms/src/server/index.tsbundled by Wrangler. - CMS Static assets —
apps/cms/dist/(the React SPA) uploaded to Cloudflare’s asset storage and served via theSTATIC_ASSETSbinding. The Worker serves API routes under/api/*and falls back toindex.htmlfor everything else. - MCP Worker —
packages/cms-mcp/bin/worker.mjsbundled by Wrangler.
Continuous deployment for documentation
The two Astro Starlight sites (docs/usage-docs, docs/dev-docs)
deploy automatically to Cloudflare Pages, mirroring the worker’s
PR → staging → production lifecycle:
| Trigger | Pages branch | URL pattern |
|---|---|---|
| PR opens / pushes | pr-<n>-<branch> | https://pr-12-foo.alokai-cms-dev-docs.pages.dev (auto) |
Push to main | staging | https://staging.alokai-cms-dev-docs.pages.dev |
Manual Deploy to Production workflow | main | https://cms-devs.alokai-apps.com (custom domain) |
All three flows are paths-filtered: if docs/usage-docs/** and
docs/dev-docs/** are unchanged versus the relevant baseline (the PR
base, the previous main commit, or the previous cms-v* tag), the
deploy step skips entirely so the Cloudflare Pages dashboard doesn’t
fill up with no-op deployments.
On PRs, the workflow comments back with the unique preview URLs so reviewers can click through. The branch slug is sanitized to Cloudflare’s allowed character set and capped to 28 chars.
Manual local fallback (still works when CI is unavailable):
yarn deploy:usage-docs # → alokai-cms-docs project, --branch mainyarn deploy:dev-docs # → alokai-cms-dev-docs project, --branch main