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>
5.4 KiB
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; seesrc/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-Secretheader, compared via timing-safe equality againstWEBSITE_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 withX-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 itsCRM_PUBLIC_URLpoints at the wrong env.
Pre-cutover checklist (CRM side — done)
/api/public/berthsserves 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-inquiriesPOST handler exists, gated onWEBSITE_INTAKE_SECRET.WEBSITE_INTAKE_SECRETdocumented in.env.example.
Pre-cutover checklist (website repo — owed)
- Generate a strong shared secret (
openssl rand -hex 32) and setCRM_INTAKE_SECRET(website) andWEBSITE_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
POSTto${CRM_PUBLIC_URL}/api/public/website-inquirieswith theX-Intake-Secretheader. 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:
- Marketing site reads from BOTH feeds for any change-detection or reconciliation jobs (or just CRM if reads can flip atomically).
- 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.
- 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.
- 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)
- Stop the NocoDB-side writes from the website (drop the dual write).
- Stop the NocoDB-side reads from the website (CRM-only).
- Mark the NocoDB Berths table read-only via NocoDB ACL.
- 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_URLto 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/berthsbefore any berth is permanently removed. (Not blocking the cutover; flagged in the audit.) - CRM-edit drift vs re-imports —
scripts/import-berths-from-nocodb.tsskips rows whereupdated_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 theupdated_atguard means a full re-import only overwrites when the rep explicitly passes--force. Decommission the script once cutover is irreversible. - 5-minute cache —
s-maxage=300on/api/public/berthsmeans 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.