Tools

A Bridge Between Learning Platforms

Migrating and synchronizing users and courses between an old platform and a new one

2 min read

I had several WordPress installations with LearnDash —courses across multiple setups— and everything needed to be moved to a single Moodle. It wasn't a one-off migration: beyond migrating, it also had to keep syncing. From that need came a bridge between the two platforms, and the most serious project I've done on my own.

The real problem wasn't the courses

Migrating courses is mechanical. The problem was the people: the same student existed in several installations with the same email, and if you didn't handle it carefully you'd end up duplicating accounts or, worse, losing the progress they already had. That's why the central decision of the project was that the email is the unique identity, and deduplication (figuring out when two records belong to the same person) happens in a canonical system of its own: WordPress is the data source, the bridge is the canonical system, and Moodle is the final training platform.

How it's built

A custom WordPress plugin (an add-on that extends it) exposes LearnDash data over REST, a standard way to request data from another program: courses, users, enrollments, and progress, with pagination and date filters for incremental sync (only what has changed since the last run). Enrollments are detected three times so nothing gets lost: WooCommerce orders, native LearnDash ones, and the activity table. In between, a control panel built with FastAPI, React, and PostgreSQL, along with Celery and Redis (a task queue) so long-running operations execute in the background: the API never blocks, and every run is recorded with its logs and can be retried if it fails. Moving data between systems wasn't new to me —I had already automated documents between Excel and XML, as I told in Document Automation: Excel and XML— but this was a different scale.

Lessons I learned the hard way

The first one: idempotency or death. Re-running an import must never produce duplicates (idempotency: repeating the operation doesn't change the result), because in practice it always gets re-run. The second: never trust the order of things; mapping is by slug or by ID, never by position. And the third: dry-run before writing (simulate without touching anything). Every change against the production Moodle is simulated first, validated in the panel, and only then applied. When you're working with real student progress, simulation isn't a design opinion: it's the difference between an incident and a calm workday.

What I'd do differently today

I'd start with the phase plan, which I was writing as the project grew, and I'd be stricter about closing the initial scope: subscriptions and SSO (a single sign-on across all systems) I left out for a good reason, and I should have been even more honest about it. And I'd set up integration tests against a test Moodle from day one, because the official API throws surprises at you too.