Theory Time: Migrating 6.5 Years of Music Education from Thinkific to LearnDash
13 courses. 1,439 enrollments. 691 videos. 63 quizzes. 6.5 years of purchase history preserved per-user. A music education platform's full move from Thinkific to LearnDash — shipped to production, including the two post-launch bugs we caught and fixed.
The platform
Theory Time is a music theory education business serving students and teachers worldwide. The catalog runs from the Theory Time Primer for absolute beginners through twelve graded curriculum levels (Theory Time Grade 1 through Theory Time Grade 12) and the Medallion Series — Bronze, Silver, Gold, Platinum, Diamond — for accelerated study. Each level mixes recorded video instruction, downloadable PDF workbooks, and chapter-end quizzes.
What we inherited
A live, revenue-generating Thinkific instance with six and a half years of enrollment history (the earliest purchase: May 31, 2019) and a customer base of thousands. Course content was a mix of Wistia-hosted videos and PDF workbooks served from Thinkific's own infrastructure. Every active customer had a one-year course access window from their original purchase date that needed to be preserved exactly — losing that data would mean either cutting customers off early or extending free access by months.
The scope we negotiated
Import all users (not just active), merge duplicate emails against pre-existing WordPress accounts, preserve original purchase dates as per-user course access expiration in LearnDash, migrate quiz questions and answers but not historical quiz attempts, switch PDF hosting from Thinkific to client-controlled S3, swap video hosting to a client-owned S3 + CloudFront setup, and run the final cutover with delta sync to catch any last-minute Thinkific activity. Locked LOE: 36–52 hours over 3–4 weeks.
Engineering: the platform-neutral exporter
We built a PHP CLI exporter that talks to Thinkific's REST and GraphQL APIs and writes platform-neutral JSON to disk, re-runnable to catch deltas. REST handled users, custom profile field definitions, and enrollments (where `expiry_date` and `activated_at` live). GraphQL handled course structure, PDF URLs, and quiz Q&A. The exporter ran each course independently — single-course mode for testing, full-catalog mode for production. Every record was idempotent on rerun.
Engineering: the LearnDash importer
On the LearnDash side: 13 courses created as `sfwd-courses` posts, hundreds of section headings and individual lessons created as `sfwd-lessons` (no Topics — Theory Time's curriculum doesn't nest that deep), and 63 quizzes (213 questions, 587 choices) attached via the quiz-holder lesson pattern because Thinkific's lesson-level quizzes don't map directly to LearnDash's separate post-type model. Course access set via `ld_update_course_access()` rather than direct postmeta writes, so back-dated enrollment windows survived LearnDash's internal recalculation. Free preview lessons mapped to LearnDash's native sample-lesson flag.
Videos: 691 files to client-owned S3
Videos extracted via Thinkific's GraphQL VideoContent type (the URL field exposes direct MP4 CDN links), downloaded concurrently, and uploaded to the client's S3 bucket with CloudFront for delivery. The entire 691-video migration completed in under four hours. Final hosting cost runs roughly $15/month in S3 storage plus pay-per-GB CloudFront bandwidth — versus $65/month for Vimeo Advanced with upload quotas that would have throttled this catalog for over a year.
PDFs: the controlled-access pattern
PDFs moved out of Thinkific's storage and into a client-owned S3 bucket with a referrer-restricted bucket policy. PDFs render inline in LearnDash lessons through a custom WordPress block that uses PDF.js bundled locally (no CDN dependency). Native browser download buttons are hidden by the custom viewer; the underlying URLs are unreachable from outside the LMS without spoofing the referrer header. The client knowingly accepted this as friction-not-prevention — the right call for their threat model, openly scoped in the proposal.
Cutover & shipped outcome
Cutover went live on schedule. 1,439 enrollment records imported with original Thinkific purchase dates intact; verification ran 69 checks across 13 courses and passed all 69. Every customer's remaining access window — whether 11 months from a recent purchase or three days from an expiring annual — moved to LearnDash unchanged. Course content, including video lessons, PDF workbooks, and quiz banks, rendered in the new LMS exactly as it had in the old one. The delta-sync pattern caught the dozen-or-so purchases that happened in the cutover window so no customer experienced a gap.
Two bugs we caught in the weeks after launch
First: imported quiz question-to-answer mappings were stored by their position in the quiz (1, 2, 3…) instead of by ProQuiz's actual question IDs. Some quizzes pulled the wrong question/answer pairs as a result. We repaired the mappings in the database and patched the importer to use real IDs. Second: every imported question's top-level `wp_learndash_pro_quiz_question.points` value was `0`, even though answer-level points were `1`/`0` correctly. LearnDash's frontend result screen uses the question-level value, not the answer-level one, so students saw "3 of 3 questions correct" alongside "0 of 0 points." Fix: set `points = 1` on every migrated question (Thinkific's grading is one-question-equals-one-point) and patch the importer to insert `points => 1` going forward. Both fixes were applied first on staging with backup + dry-run + browser verification, then on production.
What this confirms about post-launch QA
Migrations don't end at cutover. Bugs that survive into production are usually ones that pass logical verification but fail visual verification — a quiz that grades correctly but reports the wrong total looks fine to a checksum and broken to a student. Our post-launch QA now includes browser-completing at least one quiz per course before signing off, plus database spot-checks against the ProQuiz schema's top-level point columns specifically. Both of those checks would have caught these bugs pre-launch.
What we'd recommend if you're considering this move
Start by reading what Thinkific's API actually exposes — most migration estimates are wrong because the estimator assumed REST-only and missed the GraphQL surface (or vice versa). Build idempotency in from day one; one-shot exports are how data quietly goes missing during cutover. Don't promise to migrate things the source platform doesn't actually expose (full quiz attempt history is the most common over-promise). Get the client's PDF and video hosting decisions made before you write line one of code — they shape the entire migration plan. And budget time for post-launch QA, because the bugs that survive are the ones that need a real student-eye-view to catch.