Production Deployment Runbook
How to deploy PulseGate Worker and Dashboard to production and how to roll back. For architecture and endpoints, see Architecture and API Reference.
On this page: Launch checklist · Prerequisites · GitHub Actions · Custom domains · Order of operations · Rollback · Summary
Production launch checklist
Before go-live with your team:
- [ ] All tests pass:
npm test - [ ] Production secrets set:
ANALYTICS_API_KEY,CRM_CALLBACK_SECRET; optionallyCRM_WEBHOOK_SECRET,CRM_LEAD_URL_TEMPLATE,ANALYTICS_MARKETING_API_KEY(or use dashboard-generated marketing keys) - [ ] D1 migrations applied:
./scripts/apply-production-migrations.sh(covers 001–035) - [ ] Worker URL and dashboard URL documented (custom domain or workers.dev/pages.dev)
- [ ] Make.com configured: webhook URL, callback URL, secrets; CRM lead URL template in Settings if using "View in CRM"
- [ ] Dashboard Connect: team has Worker URL + admin API key; marketing keys created in Settings if needed
- [ ] Post-launch: one test lead submitted; lead visible in dashboard; optional CRM callback + webhook verified
Prerequisites
- Cloudflare account with Workers, D1, KV, R2, and Pages enabled
- Wrangler CLI installed and authenticated (
wrangler login) - Node.js 18+ (for dashboard build)
- All tests passing:
npm test
Pre-deploy checklist
- [ ]
npm testpasses (root and dashboard if applicable) - [ ] Production secrets set (see Secrets)
- [ ] Production D1 migrations applied (see D1 migrations)
- [ ] Production Worker URL known (custom domain or
https://pulsegate.<account>.workers.dev)
GitHub Actions (fully automated)
You can deploy without using the command line: push to main or run the workflow manually from the Actions tab.
What the workflow does
The workflow only runs deploy steps for what changed (so you don’t run every deploy on every push).
- Detect changes — Compares the current push to the previous commit (or, on manual run, deploys everything). Sets which targets need a deploy:
- Worker:
src/,wrangler.toml, rootpackage.json/package-lock.json,scripts/,migrations/,test/ - Dashboard:
dashboard/ - Test app:
test-app/ - Docs:
docs/
- Worker:
- Test — Runs only when Worker, Dashboard, or Test App areas changed (not for docs-only). Runs
npm ci,npm run build:form, thennpm test(must pass). - Deploy (only the jobs whose paths changed; app deploys run only if tests pass):
- Deploy Worker (if Worker paths changed): build form script, apply D1 migrations (best-effort), deploy Worker, re-apply Worker secrets.
- Deploy Dashboard (if
dashboard/changed): build dashboard, deploy to Cloudflare Pages pulsegate-dashboard. - Deploy Test App (if
test-app/changed): deploytest-app/to Cloudflare Pages pulsegate-test-app. - Deploy Docs (if
docs/changed): build VitePress docs (npm run docs:build), deploy to Cloudflare Pages pulsegate-docs. Does not wait on tests.
Manual run (Run workflow from the Actions tab) deploys all four (Worker, Dashboard, Test App, Docs).
Workflow file: .github/workflows/deploy-production.yml.
CI on pull requests
A separate CI workflow runs on every pull request targeting main and on push to non-main branches (e.g. feature/*, fix/*). It runs the same test steps (npm ci → npm run build:form → npm test) so branches are validated before merge. Workflow file: .github/workflows/ci.yml.
One-time setup
Important: GitHub secrets are write-only. After you save a secret, you cannot view it again. So: get both values ready (Account ID + API token), then add them to GitHub in one go. You don’t need to “remember” the values — just follow this checklist once.
Step-by-step (do once)
Get your Cloudflare Account ID (you can always see this again)
- Open Cloudflare Dashboard and log in.
- In the right-hand sidebar, find Account ID (under “Account”). Copy it, or keep the dashboard tab open.
- You can come back to this page anytime if you forget; no need to save it.
Create the API token and copy it immediately
- Go to API Tokens → Create Token.
- Use the Edit Cloudflare Workers template (or create a custom token with: Workers Scripts Edit, D1 Edit, KV Storage Edit, R2 Object Read & Write, Account Settings Read, Cloudflare Pages Edit).
- Create the token. Copy the token value right away — Cloudflare shows it only once.
- Optional but recommended: Save the token in your password manager (e.g. “Cloudflare API – pulsegate GitHub Actions”) so you can create a new token and update the GitHub secret later if you need to rotate it.
Add both values to GitHub (you won’t see them after saving)
- In your repo: Settings → Secrets and variables → Actions.
- New repository secret → Name:
CLOUDFLARE_ACCOUNT_ID→ Value: paste the Account ID from step 1 → Add secret. - New repository secret → Name:
CLOUDFLARE_API_TOKEN→ Value: paste the token from step 2 → Add secret. - After this, you cannot view or edit the secret values — only replace them. If you saved the token in a password manager, you can update
CLOUDFLARE_API_TOKENlater by creating a new token in Cloudflare and adding it as a new value for that secret.
Worker runtime secrets (recommended: store in GitHub so they survive deploys)
The deploy workflow re-applies Worker secrets from GitHub Actions secrets after each deploy. This prevents secrets from being lost when Cloudflare overwrites or clears them on redeploy.
In Settings → Secrets and variables → Actions, add these repository secrets (same values you use for the dashboard and Make.com):ANALYTICS_API_KEY— e.g.openssl rand -hex 32; dashboard and API clients use this to authenticate.CRM_CALLBACK_SECRET— value Make.com sends when calling your callback.CRM_WEBHOOK_SECRET(optional) — forPOST /webhooks/crm/lead; if unset, worker falls back toCRM_CALLBACK_SECRET.CRM_LEAD_URL_TEMPLATE(optional) — e.g.https://crm.example.com/lead/.CLOUDFLARE_ACCOUNT_ID(optional) — same 32-char account ID as above; when set, the workflow also injects it as a Worker secret so the dashboard can query Analytics Engine.CLOUDFLARE_ANALYTICS_ENGINE_API_TOKEN(optional) — API token with Account → Account Analytics → Read; enables/analytics/engine/*and/analytics/moderation/*(Lead analytics charts and Moderation page). After each run of Deploy to production, the workflow will set these on the Worker so production keeps working. If you prefer not to store them in GitHub, you can still set them manually in Cloudflare (Workers → pulsegate → Settings → Variables and Secrets) after each deploy; see Worker secrets.
Pages projects
The workflow deploys to these Cloudflare Pages projects: pulsegate-dashboard (dashboard), pulsegate-test-app (test-app), and pulsegate-docs (VitePress docs). Create these projects in Cloudflare Pages first if they don’t exist, or changeproject-namein the workflow.Optional: pre-fill Worker URL on Connect page
In Settings → Secrets and variables → Actions → Variables, addVITE_PULSEGATE_URL=https://pulsegate.pixleo.com(or your Worker URL). Variables are not secret and can be viewed/edited anytime.Logging and traces
Production uses a warn default log level. The deploy workflow sets Worker log flags from GitHub Actions variables (so you control them in the repo, not in Cloudflare):- LOG_LEADS — Repo variable (Settings → Variables → LOG_LEADS). The workflow defaults it to
trueso lead logs (form submit, webhook sent, redirects) are emitted. Set tofalseto suppress. - LOG_TRACKING — Repo variable LOG_TRACKING. Set to
trueonly if you want session/analytics/replay logs; when unset (default), the workflow does not set it and the Worker emits no tracking/analytics logs (leads-only logging). Optional: set LOG_LEVEL as a Worker secret (e.g.wrangler secret put LOG_LEVELwith valueinfoordebug) for more verbose logs. Observability is enabled inwrangler.toml; use the dashboard Traces and real-time logs when you need request-level detail.
- LOG_LEADS — Repo variable (Settings → Variables → LOG_LEADS). The workflow defaults it to
Triggers
- Push to
main— workflow runs automatically and only deploys the parts that changed (Worker, Dashboard, Test App, and/or Docs). - Manual run — Actions → Deploy to production → Run workflow — deploys all four (Worker, Dashboard, Test App, Docs).
After the workflow succeeds, only the jobs that ran are updated; no unnecessary deploys. No command-line steps required for routine deployments.
Custom domains and public vs private (pixleo.com)
Using pixleo.com as the main domain, use separate subdomains so public and internal traffic stay clearly separated.
| Entity | Role | Recommended subdomain | Purpose |
|---|---|---|---|
| Worker | Public | pulsegate.pixleo.com | Single public endpoint: form.js, tracker.js, lead capture, tracking. Embedded on customer sites and in scripts. Keep this URL the only one you expose publicly. |
| Dashboard | Private | pulsegate-app.pixleo.com | Internal admin app (analytics, leads, replay). Do not link from public sites; share only with admins. Access is already gated by API key on Connect; optionally add Cloudflare Access for extra protection. Flat subdomain (no nesting) so Cloudflare Pages custom domain works without issues. |
Why this split
- Worker = public: It has to be reachable from any site that loads form.js/tracker.js and from Make.com. Putting it on
pulsegate.pixleo.comkeeps one clear, branded URL for all public integrations. - Dashboard = private: It shows leads and analytics. Using
pulsegate-app.pixleo.com(one subdomain under pixleo.com) avoids nested subdomains likeapp.pulsegate.pixleo.com, which some products handle inconsistently. Flat subdomains are universally supported by Cloudflare Pages and DNS.
Setup
- Worker: In Cloudflare Dashboard → Workers & Pages → your worker → Settings → Domains & Routes → Add custom domain → pulsegate.pixleo.com. Ensure
pixleo.comis a zone in the same account (or add the zone first). - Dashboard: In Pages → your dashboard project → Custom domains → Add pulsegate-app.pixleo.com. Add the CNAME target shown by Cloudflare in your DNS for pixleo.com (e.g.
pulsegate-app→<project>.pages.dev).
After go-live, use the custom URLs everywhere (dashboard Connect page default, docs, Make.com) so the default Worker URL is https://pulsegate.pixleo.com and the dashboard is https://pulsegate-app.pixleo.com.
Order of operations
1. Worker secrets
Worker secrets (ANALYTICS_API_KEY, CRM_CALLBACK_SECRET, etc.) are stored in Cloudflare and are write-only: after you set them, you cannot view them again. So save each value somewhere safe (e.g. password manager) before you set it, then set it. You won’t need to remember the value — you only need it once to paste in, and again if you rotate.
Recommended: Add ANALYTICS_API_KEY and CRM_CALLBACK_SECRET (and optionally CRM_LEAD_URL_TEMPLATE, CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_ANALYTICS_ENGINE_API_TOKEN) as GitHub Actions repository secrets. The deploy workflow will re-apply them to the Worker after every deploy, so they are not lost when Cloudflare clears or overwrites secrets on redeploy. See the One-time setup step 4 above.
Step-by-step (do once per secret, if not using GitHub secrets)
Generate or choose the value
ANALYTICS_API_KEY: e.g. a long random string (e.g.openssl rand -hex 32) or a value your team agrees on. This is what the dashboard and API clients use to authenticate.CRM_CALLBACK_SECRET: same idea; Make.com will send this when calling your callback so only you/Make.com can call it.- Save each value in your password manager (e.g. “PulseGate Worker – ANALYTICS_API_KEY production”) so you can look it up for the dashboard and for rotation later.
Set the secret in Cloudflare (you won’t see it again)
From the project root:bashwrangler secret put ANALYTICS_API_KEY --env production # Paste the value when prompted; press Enter. wrangler secret put CRM_CALLBACK_SECRET --env production # Optional: wrangler secret put CRM_LEAD_URL_TEMPLATE --env production wrangler secret put CLOUDFLARE_ACCOUNT_ID --env production wrangler secret put CLOUDFLARE_ANALYTICS_ENGINE_API_TOKEN --env productionOr in Cloudflare Dashboard → Workers & Pages → pulsegate → Settings → Variables and Secrets → Add (under Encrypted); the value is not shown after saving.
Use the saved value where it’s needed
- Dashboard / API clients: Use the same
ANALYTICS_API_KEYvalue (from your password manager) when connecting — e.g. paste it into the dashboard Connect page or into env vars for API clients. - Make.com: Configure the callback step to send
CRM_CALLBACK_SECRET(e.g. in a header) using the value you saved.
- Dashboard / API clients: Use the same
Rotation: Generate a new value, save it in your password manager, then run wrangler secret put <NAME> --env production again (or update in the Cloudflare dashboard). Update the dashboard and Make.com with the new value.
2. D1 migrations
Apply migrations to the production D1 database in order. Use the script (recommended):
# From repository root — applies all migrations (001 through 035)
./scripts/apply-production-migrations.sh
# Apply only from a given migration onward (e.g. after 020 already applied)
./scripts/apply-production-migrations.sh 021The script runs each file in migrations/ in numeric order. If a migration was already applied (e.g. "duplicate column name" or "already exists"), the script treats it as skipped and continues. Production D1 name: pulsegate (see wrangler.toml [env.production]).
Verification: In Cloudflare Dashboard → D1 → pulsegate, confirm tables exist: Leads, LeadSubmissions, sessions, events, fingerprints, identity_network, and that Leads has columns such as CrmLeadId, EngagementScore, SuspicionScore, etc.
Recreate production D1 database (destructive): If the database is in a bad state (e.g. migrations keep failing with duplicate column or schema errors) and you are okay losing all data, you can delete and recreate it, then re-apply all migrations:
./scripts/recreate-production-d1.shOr with no prompt: ./scripts/recreate-production-d1.sh -y. The script deletes the remote database pulsegate, creates a new one, updates wrangler.toml with the new database_id, and runs apply-production-migrations.sh. Commit the updated wrangler.toml and redeploy the Worker so it uses the new database.
3. Deploy Worker
wrangler deploy --env productionNote the deployed URL (e.g. https://pulsegate.<account>.workers.dev).
Smoke tests:
# Health check (no auth)
curl -s https://<WORKER_URL>/health | jq .
# Lead capture accepts POST (should return 200 or 400 with validation message)
curl -s -X POST https://<WORKER_URL>/lead.capture \
-H "Content-Type: application/json" \
-d '{"ContactEmail":"test@example.com","ProjectName":"Smoke"}' | jq .
# Analytics requires API key (401 without, 200 with)
curl -s -o /dev/null -w "%{http_code}" https://<WORKER_URL>/analytics/overview
# Expect 401
curl -s -H "Authorization: Bearer YOUR_ANALYTICS_API_KEY" https://<WORKER_URL>/analytics/overview | jq .
# Expect 200 and JSON4. Deploy Dashboard
The dashboard is a static Vite/React app. Users enter the Worker URL and API key on the Connect page (stored in localStorage). No build-time Worker URL is required.
Optional: To pre-fill the Worker URL on the Connect page, set a build env in Pages (Settings → Environment variables): VITE_PULSEGATE_URL = https://pulsegate.pixleo.com. The dashboard uses this as the default when no URL is saved in localStorage, so admins opening the private dashboard see the correct public Worker URL immediately.
Option A — Cloudflare Pages (recommended)
- In Cloudflare Dashboard → Pages → Create project → Connect to Git (or upload build).
- Build settings:
- Framework preset: Vite
- Build command:
npm run build(run fromdashboard/if monorepo) orcd dashboard && npm run build - Build output directory:
dashboard/dist - Root directory:
dashboard(if repo root is project root, set root todashboard)
- Deploy. Note the Pages URL (e.g.
https://pulsegate-dashboard.pages.dev).
Option B — Local build and Wrangler Pages
cd dashboard
npm ci
npm run build
npx wrangler pages deploy dist --project-name=pulsegate-dashboardOption C — GitHub Actions
Use the GitHub Actions workflow above: push to main or run Deploy to production manually. No local deploy needed.
Smoke test: Open the dashboard URL → Connect → enter production Worker URL and production ANALYTICS_API_KEY → confirm overview and Leads load.
5. Post-launch verification
- [ ] Submit a test lead (form or
POST /lead.capture) and confirm it appears in D1 and in the dashboard Leads list. - [ ] If Make.com and CRM are configured: trigger a callback to set
CrmLeadId, then open the lead in the dashboard and use "View in CRM". - [ ] Check Cloudflare Workers analytics and D1 metrics for errors and latency.
- [ ] Logs: Workers → your worker → Logs (real-time and tail); use Observability if enabled in
wrangler.toml.
Rollback
Worker
- Redeploy previous version: In Cloudflare Dashboard → Workers & Pages →
pulsegate→ Deployments → select a previous deployment → "Rollback to this deployment". - Or from CI: redeploy the last known-good commit.
Dashboard
- In Cloudflare Dashboard → Pages → your dashboard project → Deployments → select previous deployment → "Rollback to this deployment".
D1
- Migrations are additive (new columns/tables). Prefer fixing forward with a new migration rather than rolling back schema. If you must revert a migration, do it manually (e.g.
DROP COLUMNorDROP TABLE) and document the change.
Production URLs (update after go-live)
With custom domains on pixleo.com:
- Worker (public):
https://pulsegate.pixleo.com - Dashboard (private):
https://pulsegate-app.pixleo.com
Without custom domains (Cloudflare defaults):
- Worker:
https://pulsegate.firstdoorrealty.workers.dev(live) - Dashboard:
https://<your-pages-project>.pages.dev(deploy fromdashboard/dist)
Update these in README.md and PROJECT_CONTEXT.md when production is live.
Summary
| Step | Action |
|---|---|
| 1 | Set ANALYTICS_API_KEY, CRM_CALLBACK_SECRET (and optionally CRM_WEBHOOK_SECRET, CRM_LEAD_URL_TEMPLATE) with wrangler secret put --env production or via GitHub Actions secrets |
| 2 | Apply all migrations to production D1 (scripts/apply-production-migrations.sh or manual loop) |
| 3 | Deploy Worker: wrangler deploy --env production; smoke test /health, /lead.capture, /analytics/overview |
| 4 | Deploy Dashboard to Cloudflare Pages; smoke test Connect → leads/analytics |
| 5 | E2E: form → lead → dashboard; document production URLs and where to view logs |