DarDev migrated the integration bus from Node to Go without rewriting DarDev Ops, Twenty CRM, or Listmonk by freezing fifty HTTP routes in packages/contracts and swapping only the runtime behind them. That strangler pattern—contract first, golden parity tests, side-by-side dev optional, production flip once— is how we ship backend changes on a live company platform without a week-long outage.
This guide explains the method so your team can repeat it on the next service. The shipped instance is Platform Bus V1.0.0—see platform-bus-go-migration-shipped for timeline and crm-orchestrates-bus-executes for why callers stayed unchanged. multi-product-saas-one-core explains why Domain A Ops and Domain B bus remain separate deployables.
Strangler steps we followed
- Freeze contract — bus-route-catalog.json with method, path, handler key, auth mode
- Golden tests — same URLs and status codes against Node baseline then Go
- Side-by-side — optional compose profile platform-bus-go on dev alternate port
- Wave migration — route groups P2 P3 P4 instead of big bang
- Env compatibility — keep SYNC_WORKER_URL and SYNC_WORKER_SECRET names
- Cutover P5 — platform-bus default; archive Node to archive/sync-worker-legacy/
Contract spine location
packages/contracts/bus-route-catalog.json is generated and validated—not hand-edited ad hoc. Regenerate with bash scripts/ops/validate/ops-validate.sh bus-sync-catalog. Each solution manifest under ops/solutions/manifests/ declares bus.routes entries that must match catalog handler keys. validate-bus-parity.mjs fails CI when Ops manifest references a route the catalog lacks.
JSON Schema for manifests and integration overlays lives in packages/contracts/schemas/. ops-validate json-schema validate runs in pre-commit and deploy gates. The bus embeds schemas at build time. Contract drift becomes a build failure, not a 3 a.m. production 404.
Golden test philosophy
Golden tests record expected HTTP status, critical JSON keys, and error shapes per route. During P2 through P4, CI spun Node baseline and Go candidate against the same fixture requests—redacted bodies without real bearer tokens in repo. High-risk groups—core-webhooks, services-outreach, server-discover SSH—had extra cases for fail-closed auth.
bus-proxy boundary
ops/lib/bus-proxy.ts is the only Ops code path that calls integration endpoints. Ops never imported sync-worker JavaScript modules—a rule established before Go migration and enforced by lint and architecture review. That boundary meant Platform Bus V1.0.0 required zero Ops UI rewrites: proxy URL, bearer header injection, and error mapping stayed identical.
Twenty workflows and external cron jobs call mail.dardev.net public nginx routes directly—they were always HTTP clients. Freezing paths like /api/daily/send and /api/news/articles protected those integrations.
Route group waves
P2 moved low-to-medium risk: core health, webhooks, ops-intel probes, listmonk proxy. P3 added news SQLite, daily digest SMTP, mail-jobs batch sender, services-outreach quartet. P4 finished server-discover SSH allowlist, public site contact API, team-alert, connector-writes. Ordering put highest blast-radius auth paths behind proven infrastructure and M17 security gate compliance.
Non-goals that saved the project
- Rewrite Ops or CRM or Listmonk internals
- Rename public URL paths or manifest handler keys
- Change Hesabi cloud production dependencies on dardev-vps bus
- Migrate SW-1 Node-to-TypeScript inside legacy worker
- Expose new undocumented routes before catalog update
Rollback and operations
Before P5 volume delete equivalent, compose profile could point SYNC_WORKER_URL back to Node container. After P5, rollback means redeploy previous Go image tag—not resurrecting Node without a formal incident decision. Env alias PLATFORM_BUS_URL is optional sugar; existing automation kept working with legacy names.
Quality gates after cutover: go run ./cmd/ops-validate platform-go-status --validate, fifty-route production verifier, M17 curl smoke, ops remote e2e. Tracker ops/tracker/platform-go.json marks each milestone done with dates for audit.
Applying strangler elsewhere
When replacing any integration service—license control plane Node to Go, future scraper workers, new webhooks—start with cataloged HTTP or gRPC contracts, not parallel ad-hoc fetches from Ops components. Add handler to catalog, implement in new runtime, golden test, flip default service name in compose, archive old tree. DarDev license plane already followed Go plus SQLite pattern on separate VPS path.
DarDev Ops customers evaluating DarDevOps solution should expect contract-first migrations: your operators keep the same console while backends evolve. That is the operational promise behind frozen HTTP contracts—not technology churn for its own sake.
Anti-patterns we avoided
- Shared npm package imported by Ops and worker—creates language lock-in
- Silent path renames with nginx rewrite magic—hides drift from manifest parity checks
- Big-bang weekend cutover without golden tests—high rollback cost
- Embedding SMTP credentials in Ops SQLite—violates orchestrates-never-configures-mail rule
- Publishing bearer tokens in docs or news articles—M17 fail-closed auth depends on secrecy
Contract-first migration trades short-term velocity for long-term replaceability. The fifty-route catalog is boring infrastructure until the next runtime swap—then it pays for itself in hours not weeks of caller fixes.
New engineers onboarding to DarDev Ops should read packages/contracts/README if present and run ops-validate bus-sync-catalog before adding a handler key—treat the catalog diff as part of the code review, not optional metadata.
What does frozen HTTP contract mean?
Route paths, methods, handler keys, and external response shapes stay stable while implementation language or internals change.
Where is the route catalog?
packages/contracts/bus-route-catalog.json, regenerated via ops-validate bus-sync-catalog.
Did strangler migration require Ops downtime?
No full-platform outage. Bus service restarted on deploy; callers retried HTTP. Golden tests caught parity breaks pre-production.
Why keep SYNC_WORKER env var names?
Backward compatibility for compose, cron, and Twenty workflow configs already pointing at mail.dardev.net bus URL.
What fails if manifest bus.routes drifts from catalog?
validate-bus-parity.mjs and ops-validate gates fail CI—deploy blocked until manifest and catalog realign.



