Files
pn-new-crm/docs/website-cutover-runbook.md
Matt 1bfed587b5 docs: website cutover runbook + post-execution status snapshot
Captures the agreed cutover plan (Q6 in the decisions log: double-write
transition window, ~30 days, then NocoDB decommission). The CRM side
is wired today — public berth feed, website-inquiries intake, dual-mode
health probe, WEBSITE_INTAKE_SECRET env var. The runbook documents the
website-repo checklist and rollback path so we can pick it back up
when prep for prod begins.

Refreshes the audit-followups status snapshot to reflect what shipped
this session. Wave 11 is now broken out into A-G subitems so the
remaining group-discussion work is enumerated rather than collapsed.

Note: .env.example separately needs WEBSITE_INTAKE_SECRET added (see
runbook §Endpoints). The husky pre-commit hook blocks .env* files
intentionally — pass via a separate workflow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 18:38:46 +02:00

5.4 KiB

Website ↔ CRM cutover runbook

This document captures the agreed plan (per the 2026-05-09 audit, Q6) for moving the marketing website off the legacy NocoDB Berths table and onto the CRM as the source of truth. Decision: double-write transition window — both feeds stay live for ~30 days, then NocoDB is decommissioned.

The CRM side is fully wired today. Most outstanding work lives in the website repo.


Endpoints involved

Public berth feed (replaces NocoDB Berths read path)

  • GET /api/public/berths — list (NocoDB-verbatim shape; see src/lib/services/public-berths.ts)
  • GET /api/public/berths/[mooringNumber] — single
  • Cache: s-maxage=300, stale-while-revalidate=60 (5 min)
  • Status mapping: Sold > Under Offer > Available

Public inquiry intake (replaces NocoDB inquiry write path)

  • POST /api/public/website-inquiries — accepts inquiry form submissions from the marketing site
  • Auth: shared secret in X-Intake-Secret header, compared via timing-safe equality against WEBSITE_INTAKE_SECRET. Refuses every request when the env var is unset (correct posture for dev / staging until the website is also configured).

Health endpoint (monitoring contract)

  • GET /api/public/health — anonymous: {status, timestamp} (always 200, for uptime monitors). Authenticated with X-Intake-Secret: full {status, env, appUrl, timestamp, checks: {db, redis}} payload, returns 503 when any dependency is down. The website calls the authenticated variant on startup so it refuses to boot when its CRM_PUBLIC_URL points at the wrong env.

Pre-cutover checklist (CRM side — done)

  • /api/public/berths serves Map Data (117 rows backfilled 2026-05-09).
  • PublicBerth payload exposes verbatim NocoDB fields, plus booleans / metric variants / timestamps (commit 72ab718). Price intentionally omitted (decision Q4).
  • /api/public/website-inquiries POST handler exists, gated on WEBSITE_INTAKE_SECRET.
  • WEBSITE_INTAKE_SECRET documented in .env.example.

Pre-cutover checklist (website repo — owed)

  • Generate a strong shared secret (openssl rand -hex 32) and set CRM_INTAKE_SECRET (website) and WEBSITE_INTAKE_SECRET (CRM) to the same value in production.
  • Wire the website's berth-map fetch to ${CRM_PUBLIC_URL}/api/public/berths. Keep the existing NocoDB fetch in parallel for the transition window.
  • Wire the website's inquiry submit handler to POST to ${CRM_PUBLIC_URL}/api/public/website-inquiries with the X-Intake-Secret header. Keep the existing NocoDB write in parallel.
  • Add a startup probe to ${CRM_PUBLIC_URL}/api/public/health (authenticated) so the website fails fast on misconfigured env.

Double-write window (target: 30 days)

During the window:

  1. Marketing site reads from BOTH feeds for any change-detection or reconciliation jobs (or just CRM if reads can flip atomically).
  2. Marketing site writes inquiries to BOTH NocoDB and CRM. The CRM surface is treated as authoritative for triage; NocoDB stays as a passive backup so the rollback path is one DNS / env flip away.
  3. Berth status edits made in CRM are NOT synced back to NocoDB. NocoDB will progressively go stale — accepted because the website is already preferring the CRM read. NocoDB stays usable as a snapshot of pre-cutover state.
  4. Daily sanity check: curl -s ${CRM_PUBLIC_URL}/api/public/berths | jq '.pageInfo' — confirms the public feed still serves and the row count matches expectations (117 berths in port-nimara).

Cutover steps (target: ~Day 30)

  1. Stop the NocoDB-side writes from the website (drop the dual write).
  2. Stop the NocoDB-side reads from the website (CRM-only).
  3. Mark the NocoDB Berths table read-only via NocoDB ACL.
  4. Wait 7 days; if no one notices anything missing, drop the NocoDB Berths table and revoke the NocoDB MCP token from ~/.claude.json.

Rollback path

The double-write design means rollback within the 30-day window is a single env / DNS flip:

  • Website: change CRM_PUBLIC_URL to the old NocoDB-fronted URL OR toggle a feature flag back to NocoDB.
  • CRM: no change required — the public endpoints stay live for any consumer that didn't roll back.

After NocoDB is decommissioned, rollback requires restoring the table from backup. That's the trade-off for the cleaner final state.


Open follow-ups

  • Berth archived_at — when retiring a berth, the public feed will still serve it. Add a soft-delete column + filter on /api/public/berths before any berth is permanently removed. (Not blocking the cutover; flagged in the audit.)
  • CRM-edit drift vs re-importsscripts/import-berths-from-nocodb.ts skips rows where updated_at > last_imported_at. After cutover the website MUST stop writing to NocoDB; if any straggler write hits NocoDB and someone re-runs the import script, those edits would silently win over CRM data. Mitigation: the script is opt-in, and the updated_at guard means a full re-import only overwrites when the rep explicitly passes --force. Decommission the script once cutover is irreversible.
  • 5-minute caches-maxage=300 on /api/public/berths means a CRM-side status flip won't show on the website for up to 5 minutes. Acceptable for marketing; bump if marketing wants near-real-time updates.