Skip to main content
mvp developmentMarch 11, 202615 min read

From WordPress to Modern Stack: Migrating a Fintech Platform

How we migrated a fintech mortgage platform from WordPress to a modern stack. Planning, execution, and lessons from a real production migration.

Loic Bachellerie

Senior Product Engineer

The Day We Decided to Stop Fighting WordPress

I got the call from Cannect's CTO on a Tuesday afternoon. Their mortgage broker platform had just failed a security audit for the third year running. The auditors flagged the same issues: outdated PHP dependencies, a plugin stack that nobody fully understood, and a custom Equifax API integration bolted onto WordPress with procedural PHP that had survived three different developers. The platform was processing real mortgage applications - people's homes were on the line - and the foundation had quietly rotted.

This post is the full account of what we did next. The planning, the technical decisions, the migration itself, and what we got wrong along the way. If you're staring at a legacy WordPress platform in a regulated industry and wondering whether to migrate or keep patching, this is the case study you need.

The Legacy Situation

Cannect's original platform was a classic story of organic growth gone wrong. It started as a simple marketing site on WordPress around 2018. Then a developer added a contact form. Then a loan calculator plugin. Then someone needed to pull credit bureau data, so they wrote a custom PHP plugin that called the Equifax Decision Power API directly. Then they needed application tracking, so another custom table was bolted onto the WordPress database alongside wp_posts and wp_options.

By the time I got involved, the system looked like this:

  • WordPress 5.9 running on a single EC2 instance (t3.medium)
  • 12 active plugins, 4 of which had not received updates in over two years
  • Custom PHP plugin (~3,200 lines) handling the Equifax credit bureau integration
  • 3 loan calculator tools built as separate shortcode-based PHP functions
  • MySQL database mixing WordPress core tables with 11 custom application tables
  • No staging environment - changes went directly to production
  • Manual deployment via SFTP from a developer's laptop
  • Zero test coverage

The Equifax integration was the most critical and most fragile piece. It handled credit pulls for mortgage pre-qualification, meaning it was touching sensitive financial data in a PHP file that lived in the wp-content/plugins directory alongside a coupon plugin and a broken SEO tool.

Performance was another issue. Page load times on the loan calculators regularly hit 4-6 seconds. The server spiked to 90%+ CPU during business hours when multiple brokers were running simultaneous applications. They had no queue system - every calculator submission and every Equifax API call ran synchronously in the same request.

Why We Decided to Migrate

I always push back on migration projects at first. Rewrites are expensive, risky, and easy to underestimate. I told the team to make the case clearly before I would agree to plan one. Here is what they came back with.

Security posture. The platform stored partial social insurance numbers, income verification data, and credit scores. PCI-DSS compliance was on the roadmap for the following year. WordPress with a custom plugin architecture is not where you want to be when you need SOC 2 or PCI certification. The attack surface, PHP execution, plugin vulnerabilities, no secrets management, was too large to lock down without essentially rebuilding the application anyway.

Developer velocity. Onboarding a new developer took three weeks because the codebase had no documentation, no tests, and the plugin system required understanding WordPress internals before you could change anything business-logic related. Pull requests took days because nobody could confidently assess the blast radius of a change.

Performance ceiling. They had already scaled vertically twice. The single-server architecture had no horizontal scaling path without significant rework. During peak application periods, broker sessions were timing out.

The cost of staying. This was the argument that landed. I asked the CTO to estimate the engineering hours spent each quarter on maintenance, security patching, and firefighting. The number was around 60-80 hours per quarter, roughly $18,000-$24,000 CAD in developer time, for a platform that still had security audit findings open year over year.

Migration was not cheap. But staying was also not free.

Planning the Migration

Before writing a line of new code, we spent three weeks on an audit. I want to be specific about what that audit covered, because skipping this step is how migrations fail.

Inventory everything that exists. We catalogued every endpoint, every form, every WordPress hook and filter the custom plugins used, every database table, and every external API call. We used a combination of static analysis, database introspection, and simply reading the code. The inventory revealed two integrations nobody on the current team knew were active - a third-party SMS provider and an old Slack webhook that fired on application submissions.

Identify what is actually used. The WordPress install had features that had been built and abandoned. We ran access logs against the URL inventory and found that roughly 30% of the custom shortcode pages had received zero traffic in the past six months. Those were cut entirely.

Understand the data model. The custom application tables were poorly normalized and contained several columns that existed "just in case" and were always null. Before designing the new schema, we traced every field through the codebase to understand what was actually written, read, and used downstream.

Map the Equifax integration precisely. This required the most care. The Equifax Decision Power API has strict requirements around data handling, transmission security, and response storage. We documented every request type the existing plugin made, the response fields stored, and the retention policy (which turned out to be "stored forever" - something we addressed in the new design).

With the audit complete, we decided on a phased parallel-run approach. The new platform would run alongside WordPress for roughly six weeks, with brokers gradually migrated by account. This was more expensive than a hard cutover but critical in a regulated context where a failed deployment has real consequences for real mortgage applications.

Choosing the New Stack

Fintech is not the place to experiment with bleeding-edge frameworks. The stack had to be production-proven, well-documented, and easy to hire for.

Frontend: Next.js 14 (App Router) The broker-facing portal needed server-side rendering for the initial load performance improvement and SEO-friendliness for the public-facing calculator pages. Next.js with the App Router gave us React Server Components for the static parts and client components only where interactivity was required.

API layer: Node.js with tRPC We chose tRPC over REST because the team was TypeScript-first, and having end-to-end type safety between the Next.js frontend and the API layer eliminated an entire class of bugs that had plagued the PHP codebase. The type contracts forced documentation by default.

Database: PostgreSQL on RDS (Aurora Serverless v2) PostgreSQL was the obvious choice - the data model was relational, we needed proper foreign key constraints (absent in the WordPress setup), and the financial data required ACID guarantees. Aurora Serverless v2 gave us auto-scaling without the operational overhead of managing read replicas manually.

Background jobs: BullMQ on Redis Every Equifax API call moved to an async queue. A synchronous credit pull that could fail or time out had no business running in a web request. BullMQ on ElastiCache Redis gave us job retries, priority queues, and visibility into job state that the old PHP implementation completely lacked.

Infrastructure: AWS with CDK All infrastructure as code. No more SSH access to production servers, no more SFTP deployments. CDK in TypeScript meant the infrastructure was type-checked alongside the application code.

Migration Timeline

Cannect Platform - 14 weeks total

Weeks 1–3

Audit & Planning

Inventory, data model, Equifax API mapping, architecture decisions

Weeks 4–6

Database & API Layer

Schema design, migration scripts, tRPC router setup, auth system

Weeks 7–9

Frontend & Integrations

Next.js portal, loan calculators, Equifax queue worker, SMS integration

Weeks 10–11

Parallel Run

20% of brokers on new platform, data sync from legacy, issue resolution

Weeks 12–14

Full Rollout

Remaining brokers migrated, WordPress decommissioned, monitoring hardened

The Migration Process

Database Migration

The data migration was the piece I was most cautious about. We had 11 custom tables, 6 years of application data, and zero confidence in the integrity of the existing records (the old system had no foreign key constraints, so orphaned records were common).

The migration ran in three phases. First, a read-only sync: we wrote a Node.js script that continuously pulled from the WordPress MySQL database and wrote normalized records into the new PostgreSQL schema, with an origin_legacy_id column to trace every record back to its source. This ran in the background for two weeks before any broker touched the new system.

Second, conflict resolution: we identified ~1,400 application records that failed validation against the new schema - mostly nulls in fields that the new model required, and a handful of duplicate records created by a race condition in the old submission logic. Each category got a resolution rule.

Third, cutover: when a broker was migrated to the new platform, their account became the source of truth in PostgreSQL. A thin sync layer still wrote to the legacy MySQL tables for the duration of the parallel run, so the WordPress admin views stayed accurate for the operations team until we were ready to turn them off.

API Layer

We built the tRPC API in a monorepo alongside the Next.js frontend. The router structure mapped closely to the domain objects: applications, brokers, calculators, creditPulls. Each router had explicit input validation using Zod schemas - something the PHP codebase had never done at all.

One early decision that paid off: we kept the tRPC API callable from outside Next.js by exposing an HTTP adapter. This meant future mobile clients or third-party integrations could use the same validated API rather than bypassing it.

Frontend

The public-facing loan calculators were the highest-traffic pages and the easiest win. The old PHP shortcodes rendered server-side on every request, including the full WordPress bootstrap. Moving them to React Server Components with pre-rendered static shells cut time-to-interactive from 4.2 seconds to under 800ms - before any other optimization.

The broker portal required more care. Brokers had strong muscle memory for the old interface, and we had been warned that any visual regression would generate immediate complaints. We did a side-by-side review of every screen with two brokers during the parallel run phase, and we iterated on three screens that caused friction before rolling out to the full broker base.

The Equifax API Integration

This was the most technically demanding part of the migration. The Equifax Decision Power API is not a forgiving integration - it has strict rate limits, payload validation rules, and audit trail requirements that the old PHP plugin handled inconsistently (sometimes not at all).

The new integration had four components:

1. A typed Equifax client. All API communication centralized in a single module with TypeScript types generated from the Equifax XML schema. No more procedural PHP making HTTP calls with raw curl parameters.

2. A BullMQ job queue. Credit pull requests entered a priority queue. Brokers saw an immediate "request received" state in the UI while the job processed asynchronously. If the Equifax API returned a 429 or a 5xx, the job retried with exponential backoff rather than silently failing as the PHP version often did.

3. Audit logging. Every Equifax request and response was logged to an immutable audit table in PostgreSQL - request timestamp, broker ID, application ID, response code, and a hash of the request payload. This audit trail was something the security auditors had flagged as missing for two consecutive years.

4. Data retention enforcement. The old system stored full credit responses indefinitely. The new system applied a 90-day retention policy via a scheduled job, keeping only the derived fields required for ongoing application processing. This reduced the exposure window for sensitive data significantly.

The queue approach revealed something interesting: the old PHP integration had a silent failure rate of roughly 12% - requests that errored without logging or user notification. We discovered this by replaying historical requests against the new queue and comparing outcomes. That 12% represented real applications where brokers had manually followed up with Equifax by phone without knowing it was a platform failure.

Testing and Rollout Strategy

We approached testing in layers.

Unit tests covered the Equifax client, loan calculation logic, and all tRPC router handlers. These were written before implementation using Jest. The calculator tests were particularly valuable because the old PHP implementation had three different calculation functions that produced slightly different results for the same inputs - a bug that had gone unnoticed because there were no tests to compare them.

Integration tests covered the database migration scripts and the queue worker. We used a test PostgreSQL instance seeded with sanitized production data snapshots.

End-to-end tests using Playwright covered the five critical broker flows: login, application creation, calculator submission, credit pull request, and application status update. These ran on every pull request against a staging environment that mirrored production.

For rollout, we used a feature flag system (LaunchDarkly) to control which broker accounts were directed to the new platform. We started with 5 internal test accounts, then moved to 20% of brokers (the most technically comfortable ones, selected deliberately), then 60%, then 100%. Each phase had a two-day observation window before advancing.

The parallel run revealed three issues we would not have caught otherwise: a timezone handling bug in application timestamps (the old system stored everything in Eastern time without a timezone marker; the new system used UTC), a missing field in the broker notification emails, and a performance regression in the application list view under heavy filtering. All three were fixed before the full rollout.

Before and After: The Numbers

Platform Performance Comparison

WordPress (legacy) vs. Next.js + Node.js (new)

MetricWordPressNew StackChange
Calculator page load (p50)4.2s0.78s-81%
Application submission time3.1s0.42s-86%
Equifax pull silent failure rate12%0.3%-97%
Peak CPU utilization91%23%-75%
Monthly infra cost (AWS)$340$290-15%
Dev maintenance hours (per quarter)~70h~12h-83%
Time to onboard new developer3 weeks3 days-93%
Open security audit findings70-100%

The infrastructure cost savings were smaller than the headline, but that was expected - we added managed Redis, Aurora Serverless, and a proper CI/CD pipeline. The real financial return came from reduced maintenance burden: 58 hours per quarter recovered at $150/hour is over $34,000 CAD annually in engineering capacity redirected to product development.

Lessons Learned

The audit is not optional. Every time I have skipped or abbreviated a codebase audit on a migration project, it has cost more time downstream than the audit would have. You cannot estimate migration scope accurately without knowing what is actually there. Cannect's two mystery integrations, the SMS provider and the Slack webhook, would have broken production if we had missed them.

Async-first for external APIs. Any call to a third-party API that affects user-facing state should be queued, not synchronous. The Equifax integration running in-process in a PHP web request was always a ticking clock. Moving it to a queue improved reliability, visibility, and retry behavior simultaneously.

Parallel run adds time but reduces risk. We almost skipped the parallel run to save two weeks. In retrospect, the timezone bug and the notification email regression caught during the parallel run would have caused significant broker complaints and potentially corrupted application records if we had done a hard cutover. In regulated industries, the parallel run is insurance.

Data quality debt comes due at migration time. The ~1,400 records that failed migration validation were not a new problem - they were years of data quality issues that the old system had silently accepted. A migration forces you to confront that debt. Budget for it.

The hardest part is not the technology. The hardest part was managing the transition for brokers who had been using the same interface for years. Three screens required iteration after the parallel run because we had optimized for what we thought was better UX without adequately testing with actual users. Build earlier broker feedback sessions into the plan.

Type safety pays for itself. We found six calculation errors during the tRPC + Zod migration that had existed in the PHP codebase undetected - edge cases where the loan calculator returned incorrect results for specific input combinations. TypeScript and runtime schema validation catch entire categories of bugs before they reach production.

When NOT to Migrate

I want to be direct about this, because I have seen clients talk themselves into rewrites that were not warranted.

Do not migrate if the platform is performing adequately and the primary issue is aesthetics. A WordPress site that loads in two seconds, has clean plugin management, and is maintained by people who know it well does not need a rewrite to satisfy someone's preference for React.

Do not migrate if your team cannot maintain the new stack. Cannect had TypeScript experience on their team and a hiring pipeline that favored Node.js developers. If their team were PHP-only and not planning to change that, introducing Next.js and tRPC would have traded one maintenance problem for a worse one.

Do not migrate without executive sponsorship and a realistic budget. This migration cost Cannect roughly CAD $65,000 in engineering time across 14 weeks. The ROI was clear and the payback period was under two years, but projects of this scope stall or fail when leadership expects a $10,000 cleanup job.

Do not migrate under deadline pressure. If the reason for the migration is a hard compliance deadline six weeks away, a full rewrite is the wrong tool. Targeted hardening of the existing system buys time. Do the migration properly, on a reasonable timeline, or do not do it.

Consider incremental strangling instead. The Strangler Fig pattern, gradually replacing pieces of the legacy system with new services, routing traffic incrementally, is often lower risk than a full parallel rewrite. For Cannect, the data model was too tightly coupled to the WordPress schema to strangle effectively, so a parallel build made more sense. But for systems with cleaner boundaries, incremental migration reduces the blast radius of any single failure.

Where Cannect Is Now

Eight months after the WordPress platform was decommissioned, Cannect passed their security audit for the first time. Their development team shipped four new features in the first quarter on the new platform - more than they had shipped in the previous year. The broker portal performance has not been raised as an issue since launch.

The Equifax audit log, which had been a recurring finding for three years, now passes automatically. The retention job runs on schedule. The queue worker processes credit pull requests with a 99.7% success rate, and when a failure does occur, the broker sees a meaningful error message and the operations team gets a Slack notification with the full context.

It was not a painless project. The parallel run extended by a week, the timezone bug required a data backfill, and we made some UI decisions that we had to revisit. But the system Cannect is running today is one their team can understand, extend, and trust.

That is what a migration should deliver - not just new technology, but a platform you can actually work with.


Working through a similar decision? If you have a legacy platform in fintech or another regulated space and you are trying to figure out whether to migrate, modernize in place, or keep patching, reach out through the contact form. I am happy to talk through the tradeoffs for your specific situation before you commit to anything.

Share:

Get practical engineering insights

AI voice agents, automation workflows, and shipping fast. No spam, unsubscribe anytime.