Progress · 0/22
DEPLOY v2.0 2026-05-02 · click-by-click · ~2 hr
production launch · 613parts

Deploy Runbook

Every command, every Cloudflare dashboard click, every secret in order. Check off boxes as you go — your progress saves locally to the browser so you can close the tab and resume. Each step has rollback notes if it goes sideways.

PHASE 1 of 7

Prereqs

~10 min · gates: Cloudflare account + domain on Cloudflare
STEP 0a
Cloudflare account exists, billing attached
5 min

If you don't have an account, sign up at cloudflare.com. Free tier is fine for v1.0 launch (we're well under all the rate limits).

Verify
Check before continuing
Logged into dash.cloudflare.com
You see a list of your sites (or an "Add site" button if empty)
STEP 0b
613parts.ca on Cloudflare DNS
5–60 min

If 613parts.ca isn't already in your sites list, you need to add it and switch nameservers at your registrar. This is the longest gate — DNS propagation can take 1–24 hours.

Add the site

Cloudflare will scan existing DNS records and import them. Then it'll show you 2 nameservers (something like jonas.ns.cloudflare.com and kate.ns.cloudflare.com).

Switch nameservers at your registrar

Log into wherever 613parts.ca is registered (GoDaddy, Namecheap, Domains.ca, etc.). Find DNS / nameserver settings. Replace whatever's there with the 2 Cloudflare nameservers.

Wait for propagation. Cloudflare emails you when active.

Don't proceed past Phase 4 until DNS is active You can do Phases 2 + 3 (auth, R2, KV) immediately. Worker deploys (Phase 4) need DNS so the custom domain binds. If you're in a hurry, do Phases 2-3 now and come back to Phase 4 once DNS propagates.
Verify
Check before continuing
613parts.ca appears in your Cloudflare sites list with status "Active"
dig 613parts.ca NS +short returns the Cloudflare nameservers (test from terminal)
If propagation stalls past 24 hr
Go back to your registrar and double-check the nameservers were saved (some registrars require an explicit "Save" or "Confirm" after pasting). If still stuck, contact your registrar's support — they sometimes have a custom-NS lock you didn't know about.
STEP 0c
Local repo + Node 20+ + terminal in worker/
2 min

You need the repo cloned locally and a terminal open in the worker/ subdirectory.

Verify
cd /path/to/613parts/worker
node --version  # v20.x.x or higher
ls              # should show wrangler.toml, package.json, src/
If node is too old
Install Node 20 via nvm install 20 (if you have nvm) or download from nodejs.org.
PHASE 2 of 7

Wrangler authentication

~3 min · connects your terminal to the Cloudflare account
STEP 1
Run wrangler login
2 min

From the worker/ directory:

npx wrangler login

What happens:

  1. Wrangler prints a URL and waits
  2. Your browser opens to a Cloudflare authorize page
  3. Click Allow
  4. Browser shows "Successfully logged in" → close it
  5. Terminal prints Successfully logged in.
Confirm
npx wrangler whoami
You are logged in with a Cloudflare API Token, associated with the email your@email.com.
👋 You are logged in with the email associated with the Cloudflare account: Your Account (acct-id)
Verify
Email matches your Cloudflare account
Account name is correct (if you have multiple Cloudflare accounts, the active one matters)
If wrangler login hangs or fails
Browser blocked the popup? Copy the URL from terminal and paste manually. Wrong account selected? Run npx wrangler logout first then re-login. Multiple accounts? Set the right one with export CLOUDFLARE_ACCOUNT_ID=xxx after login.
PHASE 3 of 7

Storage · R2 bucket + KV namespace

~5 min · object storage + low-latency cache
STEP 2
Create R2 bucket
1 min

Catalog data, AS400 invoice imports, computed driver routes, delivery events — all live in this bucket.

npx wrangler r2 bucket create 613parts-catalog
Creating bucket '613parts-catalog'...
Created bucket '613parts-catalog'.
Verify
Bucket appears in dashboard: Cloudflare Dashboard › R2 › 613parts-catalog
npx wrangler r2 bucket list shows the bucket
R2 pricing Free tier: 10 GB storage / 1M Class A ops / 10M Class B ops per month. Our expected usage: ~1 GB / month. Plenty of headroom.
If "Bucket already exists"
Means it's there from a prior attempt. Skip this step — nothing to fix.
STEP 3
Create KV namespace + paste ID into wrangler.toml
3 min

KV caches geocoder results so we don't re-bill Google Maps for repeat customer addresses.

npx wrangler kv:namespace create SEARCH_CACHE
🌀 Creating namespace with title "613parts-bot-worker-SEARCH_CACHE"
✨ Success!
Add the following to your configuration file:
[[kv_namespaces]]
binding = "SEARCH_CACHE"
id = "a1b2c3d4e5f67890abcdef1234567890"

Copy the id value

Open worker/wrangler.toml and replace both occurrences of "REPLACE_WITH_KV_NAMESPACE_ID" with that id:

# Top-level (used by `wrangler dev`)
[[kv_namespaces]]
binding = "SEARCH_CACHE"
id = "a1b2c3d4e5f67890abcdef1234567890"

# Production (used by `wrangler deploy --env production`)
[[env.production.kv_namespaces]]
binding = "SEARCH_CACHE"
id = "a1b2c3d4e5f67890abcdef1234567890"
Verify
grep -c REPLACE_WITH_KV_NAMESPACE_ID worker/wrangler.toml returns 0
grep -c "$KV_ID" worker/wrangler.toml returns 2 (both top-level and env.production)
Namespace appears at: Cloudflare Dashboard › Workers & Pages › KV
If you accidentally created two namespaces
Delete the extras: npx wrangler kv:namespace delete --namespace-id=<id>. Keep just one and update wrangler.toml.
PHASE 4 of 7

Worker deploy · the brain goes live

~15 min · secrets, deploy, custom domain
STEP 4
Set production secrets
10 min

Two are required for the worker to deploy and run; the rest enable specific features.

Required — set these now

JWT_SECRET · HS256 key for driver/dispatch auth tokens
# Generate a strong secret first
openssl rand -hex 32  # copy the output

# Then set it
npx wrangler secret put JWT_SECRET --env production
# prompts: Enter a secret value: paste-the-hex-string
STRIPE_SECRET · pay-link minting (test or live)

From dashboard.stripe.comDevelopersAPI keys. Start with the test key (sk_test_*). Swap for live (sk_live_*) when ready to take real money.

npx wrangler secret put STRIPE_SECRET --env production

Strongly recommended

CRON_TRIGGER_TOKEN · lets you fire the cron manually
# Generate, then set
openssl rand -hex 16
npx wrangler secret put CRON_TRIGGER_TOKEN --env production
SLACK_WEBHOOK_URL · ops notifications to #ops-quotes

In Slack: create a new app at api.slack.com/appsIncoming Webhooks → activate → add to #ops-quotes channel → copy webhook URL.

npx wrangler secret put SLACK_WEBHOOK_URL --env production
GOOGLE_MAPS_API_KEY · geocoding (40K calls/mo free tier)

From console.cloud.google.com → create project → enable Geocoding APICredentials → create API key → restrict to Geocoding API only (good hygiene).

npx wrangler secret put GOOGLE_MAPS_API_KEY --env production

Optional — set when ready

POSITRACE_API_URLLive truck GPS (falls back to phone GPS without)
POSITRACE_API_KEYPro-tier API token from PosiTrace support
POSITRACE_VEHICLESFormat: VEHICLE_4567:belleville,VEHICLE_8901:kingston
Never set GEOCODE_STUB in production That env var is for local dev only. If it leaks into prod, every address gets fake coordinates and routes break.
Verify all required secrets are set
npx wrangler secret list --env production
Output includes JWT_SECRET
Output includes STRIPE_SECRET
Output includes CRON_TRIGGER_TOKEN (recommended)
Output includes SLACK_WEBHOOK_URL (recommended)
Output includes GOOGLE_MAPS_API_KEY (recommended)
If you set a secret with the wrong value
Just run the same wrangler secret put command again with the correct value — it overwrites. To delete: wrangler secret delete SECRET_NAME --env production.
STEP 5
Deploy the worker
3 min

From worker/ directory:

npm install                           # first time only
npm run typecheck                     # should pass cleanly (0 errors)
npm run test:unit                     # 49 tests passing
npx wrangler deploy --env production
Total Upload: ~250 KiB / gzip: ~80 KiB
Uploaded 613parts-bot-worker (2.1 sec)
Published 613parts-bot-worker (3.4 sec)
  bot.613parts.ca (custom domain)
Current Deployment ID: a1b2c3d4-...
Verify (workers.dev URL works even before custom domain)
curl https://613parts-bot-worker.your-account.workers.dev/health
Returns ok
Worker visible in dashboard at Workers & Pages › 613parts-bot-worker
If deploy fails with "KV namespace not found"
Step 3 was incomplete. Open worker/wrangler.toml and verify both KV id fields are set to a real namespace ID, not REPLACE_WITH_KV_NAMESPACE_ID.
If deploy fails with "Authentication failed"
Re-run npx wrangler login. If you have multiple accounts, ensure the right one is active.
STEP 6
Bind bot.613parts.ca to the worker
3 min

The worker is up at the workers.dev URL. Now route the production custom domain to it.

OR via the worker's settings:

FieldValue
Domainbot.613parts.ca
Environmentproduction

Cloudflare auto-creates the DNS record + provisions an SSL cert. Takes ~30 sec.

Verify
curl https://bot.613parts.ca/health
Returns ok
SSL cert is valid (browser shows lock icon, not warning)
If "Custom domain not active" after 5 min
DNS propagation is slow. Wait another 5 min and refresh dashboard. Still red? Verify Phase 1 Step 0b is fully done — if 613parts.ca isn't on Cloudflare DNS, custom domains can't be auto-provisioned.
PHASE 5 of 7

Pages deploys · driver + dispatch apps

~15 min · two static sites + custom domains
STEP 7
Create both Pages projects
3 min

One project per app. From repo root:

cd ../driver-app
npx wrangler pages project create 613parts-driver-app

cd ../dispatch-app
npx wrangler pages project create 613parts-dispatch-app
Production branch Wrangler will ask "What is the name of your production branch?" — type main (or whatever your default branch is).
Verify
Both projects appear at Workers & Pages › Overview
Each has a placeholder URL like 613parts-driver-app.pages.dev
STEP 8
Build + deploy driver-app
4 min
cd driver-app
npm install
npm run build                         # NOT build:preview — want real API
npx wrangler pages deploy dist --project-name=613parts-driver-app
What gets bundled The dist/ folder includes the _redirects file we shipped (SPA routing) and the PWA service worker. Both deploy automatically.
Verify
Deploy URL printed (e.g. https://abc123.613parts-driver-app.pages.dev)
Open the URL → driver login screen renders
Direct nav to /login path doesn't 404 (proves _redirects works)
STEP 9
Build + deploy dispatch-app
3 min
cd ../dispatch-app
npm install
npm run build
npx wrangler pages deploy dist --project-name=613parts-dispatch-app
Verify
Deploy URL works → dispatch login screen renders
Direct nav to /dashboard path doesn't 404
STEP 10
Bind drive. and dispatch. custom domains
5 min

Each Pages project gets its own custom domain. Same flow as worker (Step 6) but on Pages.

For driver-app

Enter: drive.613parts.ca

For dispatch-app

Enter: dispatch.613parts.ca

Cloudflare provisions DNS + SSL in ~60 seconds for each.

Verify
https://drive.613parts.ca loads driver login (with valid SSL)
https://dispatch.613parts.ca loads dispatch login (with valid SSL)
Both domains show "Active" status under Custom Domains tab
If custom domains stay "Pending" past 10 min
Verify the parent domain (613parts.ca) is on Cloudflare DNS (Phase 1 Step 0b). Subdomains can't auto-provision if the parent isn't managed by Cloudflare.
PHASE 6 of 7

Smoke test · prove it works end-to-end

~5 min · curl everything
STEP 11
5-command smoke test
5 min
1 · Worker health
curl https://bot.613parts.ca/health
Returns: ok
2 · Search endpoint (catalog empty → graceful "no match")
curl -X POST https://bot.613parts.ca/api/v1/er-search \
  -H 'content-type: application/json' \
  -d '{"cf_query":"oil filter"}'
Returns 200 with JSON containing "messages" array (empty catalog says "couldn't find a match")
3 · Driver app loads
curl -I https://drive.613parts.ca
Returns HTTP/2 200 with content-type: text/html
4 · Dispatch app loads
curl -I https://dispatch.613parts.ca
Returns HTTP/2 200
5 · Manual cron trigger (proves auth + Slack work)
curl -X POST https://bot.613parts.ca/cron/run \
  -H "authorization: Bearer $CRON_TRIGGER_TOKEN"
Returns cron triggered (status 202)
Slack #ops-quotes channel receives a notification within ~30 sec
PHASE 7 of 7

ManyChat configuration

~90 min · separate task, can defer if needed
STEP 12
Wire the bot per the existing flow guide
90 min

This is a separate clicking-through-ManyChat task. Worker URLs are already production-correct (bot.613parts.ca) so the External Request bodies in the guide work as-is.

Open the flow guide:

That doc has 12 sub-steps: 25 Custom Fields, 4 keyword triggers, 5 flows, postback dispatcher, smart filter for live-chat. Plan ~90 min uninterrupted.

Verify after ManyChat setup
Send a test message ("hi") to the Page → bot replies with welcome quick-replies
Send "oil filter" → bot returns search response (or "no match" if catalog empty)
Tap "Talk to a human" → conversation appears in ManyChat inbox under "Needs human" filter
POST-LAUNCH

Operations & rollback

reference · keep this section bookmarked
REFERENCE
Live logs · rollback · manual cron

Stream live logs from the worker

npx wrangler tail --env production

Useful when something breaks in production and you need to see what's happening in real-time.

Roll back the worker to a previous deploy

npx wrangler deployments list --env production
# Pick a previous deployment ID, then:
npx wrangler rollback [deployment-id] --env production

Roll back a Pages app

Manually trigger the catalog/invoice cron

curl -X POST https://bot.613parts.ca/cron/run \
  -H "authorization: Bearer $CRON_TRIGGER_TOKEN"

Used when AS400 missed the nightly run and you need today's routes processed manually.

Force a worker redeploy without code changes

cd worker
npx wrangler deploy --env production --compatibility-date=$(date +%Y-%m-%d)

Update a single secret

npx wrangler secret put SECRET_NAME --env production
# Overwrites whatever was there. No downtime.
TROUBLESHOOTING

Common errors

5 most likely failures + 1-line fixes
"KV namespace 'REPLACE_WITH_KV_NAMESPACE_ID' not found" Step 3 wasn't completed. Edit worker/wrangler.toml and replace BOTH placeholder occurrences with the real ID from wrangler kv:namespace create. Redeploy.
"R2 bucket '613parts-catalog' not found" Step 2 was skipped, OR the bucket exists in a different account. Run npx wrangler r2 bucket list; if it doesn't appear, you might be on the wrong account — check wrangler whoami.
Custom domain stays "Pending" or "Inactive" Almost always: Phase 1 Step 0b incomplete. The parent domain 613parts.ca must be on Cloudflare DNS (active, not just added) for subdomain auto-provisioning. Wait for nameserver propagation; verify with dig 613parts.ca NS +short.
Driver app: 404 on direct nav to /login The _redirects file didn't ship in dist/. Confirm: cat driver-app/dist/_redirects — should output /* /index.html 200. If missing, rebuild: cd driver-app && npm run build.
Browser console: "blocked by CORS policy" Origin not on the allowlist in worker/src/lib/cors.ts. For now: hit drive.613parts.ca exactly (not www. or a different port). To add new origins, edit the ALLOWED_ORIGINS set + redeploy worker.
"Authentication failed" on wrangler commands Token expired or wrong account active. npx wrangler logout then npx wrangler login fresh.
FUTURE

What's next after launch

v1.1 – v1.3 features in priority order
PhaseTriggerAction
v1.0 lock First IN3812D sample lands in R2 Validate column names + payment enum against locked contract; tighten alias map; flip on prod cron at 04:00 EST.
v1.1 inline checkout Stripe live keys verified Build er-mint-stripe handler — replaces BUY_<sku> → human-handoff fallback with a live pay-link.
v1.2 VIN decode NHTSA API verified Build er-decode-vin — lets customers paste a VIN instead of using the year/make/model picker.
v1.3 returns pickup Driver UX feedback Emit returns to a separate pickups/{date}/{branch}.json route file.