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.

Migration

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

  1. Stand up Postgres tables for the domain via Drizzle migration. Empty.
  2. One-shot backfill — script reads SQL Server, normalizes, writes Postgres. Idempotent (run multiple times safely).
  3. Build v2 UI + API for the domain. Reads only from Postgres.
  4. 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.
  5. Cutover writes: v2 writes Postgres only. v1 stops being source of truth for this domain.
  6. Reverse CDC (optional): Postgres → SQL Server so XAF UI can still display data for unmigrated cross-references.
  7. 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:

  1. Shared session table. Both v1 and v2 read/write the same Postgres sessions table. Requires XAF code change. Skip — defeats the point.
  2. JWT bridge. XAF logout button issues a one-time token; v2 accepts it on login. Some XAF code change.
  3. 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)

  1. Identity + Property — foundational, no cross-deps from unmigrated domains.
  2. Contracts — depends on Identity + Property.
  3. Billing — depends on Contracts.
  4. Payments + Bank recon + eBarimt — depends on Billing.
  5. Chatbot + Reports + Files.
  6. Decommission — mobile cutover, archive v1.

Risks + mitigations

RiskMitigation
Data drift during dual-writeNightly parity check script: compare row counts + sample rows; alert on divergence
Mobile app stranded between v1 + v2 APIsMobile keeps hitting SpacehubApiV2 until Phase 6; no rush
Report fidelity mismatch confuses accountantsKeep v1 read-only access during Phase 3-5; users can pull v1 PDFs as fallback
Auth session split confusionRe-login on v2; clear messaging in UI
Backfill takes longer than expectedRun backfill in batches; observable progress (Postgres has pg_stat_progress_*)

Decommission criteria (Phase 6)