Strangler fig migration
v1 (XAF Blazor / SQL Server) and v2 (Next + Hono / Postgres) run side-by-side. Each phase moves a domain from v1 to v2. Users stay on v1 for unmigrated domains. No big-bang flag day.
Topology
┌─────────────────┐
Users → │ Reverse proxy │
│ (Traefik on │
│ Hetzner cron) │
└─┬────────┬──────┘
│ │
/v2/* + /(new domains)│ /(legacy domains)
│ │
▼ ▼
┌──────────────────┐ ┌────────────────────────┐
│ Next.js v2 + API │ │ XAF Blazor (Spacehub23)│
│ Postgres │ │ SQL Server │
└────────┬─────────┘ └────────┬───────────────┘
│ │
│ dual-write │
│ ─────── OR ─────── │
│ Debezium CDC │
▼ ▼
Source of truth shifts per-domain as it migrates
Per-domain migration playbook
- Stand up Postgres tables for the domain via Drizzle migration. Empty.
- One-shot backfill — script reads SQL Server, normalizes, writes Postgres. Idempotent (run multiple times safely).
- Build v2 UI + API for the domain. Reads only from Postgres.
- Dual-write phase (2 weeks): every write in v2 also writes to SQL Server via a shim. OR turn on Debezium CDC SQL Server → Postgres so old XAF stays source of truth temporarily.
- Cutover writes: v2 writes Postgres only. v1 stops being source of truth for this domain.
- Reverse CDC (optional): Postgres → SQL Server so XAF UI can still display data for unmigrated cross-references.
- Drop legacy UI once XAF UI for this domain is no longer used.
Strategy choice — recommended path
Default: dual-write only. Skip CDC.
Less operational complexity. CDC (Debezium + Kafka Connect / Redpanda) is a significant infra add for a few months of dual-system runtime. Only add CDC if you find XAF UI must continue showing data for a migrated domain — which we expect to avoid by porting in dependency order.
Routing during transition
Traefik (or Cloudflare Workers) routes by hostname + path:
# traefik rules
- host: app.spacehub.mn → next.js v2 (catches all migrated routes)
- host: app.spacehub.mn, path: /Statement_ListView* → blazor v1 (until billing phase done)
- host: api.spacehub.mn → hono v2 (new mobile traffic)
- host: api.v1.spacehub.mn → SpacehubApiV2 .NET (existing mobile, until cutover)
Auth bridge
Three options, decreasing complexity:
- Shared session table. Both v1 and v2 read/write the same Postgres
sessionstable. Requires XAF code change. Skip — defeats the point. - JWT bridge. XAF logout button issues a one-time token; v2 accepts it on login. Some XAF code change.
- Re-login on v2. Users log in fresh on v2. Small friction; zero XAF changes. Recommended for small user base.
Migration order (recap from phases)
- Identity + Property — foundational, no cross-deps from unmigrated domains.
- Contracts — depends on Identity + Property.
- Billing — depends on Contracts.
- Payments + Bank recon + eBarimt — depends on Billing.
- Chatbot + Reports + Files.
- Decommission — mobile cutover, archive v1.
Risks + mitigations
| Risk | Mitigation |
|---|---|
| Data drift during dual-write | Nightly parity check script: compare row counts + sample rows; alert on divergence |
| Mobile app stranded between v1 + v2 APIs | Mobile keeps hitting SpacehubApiV2 until Phase 6; no rush |
| Report fidelity mismatch confuses accountants | Keep v1 read-only access during Phase 3-5; users can pull v1 PDFs as fallback |
| Auth session split confusion | Re-login on v2; clear messaging in UI |
| Backfill takes longer than expected | Run backfill in batches; observable progress (Postgres has pg_stat_progress_*) |
Decommission criteria (Phase 6)
- All v1 routes have v2 equivalents and are no longer in user traffic logs for 30 days.
- Mobile app version pointed at v2 has >95% adoption.
- Final SQL Server snapshot taken + archived to R2 (12-month retention).
- XAF Blazor + Automation hosts shut down.
- SQL Server license cancelled.