← All posts
Sunday, April 26, 2026·6 min read

Migrating live lesson subscriptions from WooCommerce to Stripe in one afternoon

lessonsstripecase study

One of the trainers in our barn had been running her lesson billing on a WordPress site with WooCommerce Subscriptions and a Stripe gateway plugin. It worked, until it didn't. Plugin updates broke things. The shop UI was a maintenance burden. Cancelling a student required clicking through three admin pages. And nothing talked to the rest of the barn — student rosters lived in WooCommerce, makeup credits lived in her head, and the barn schedule had no idea who any of those students were.

We replaced it with native lesson billing in our platform — not by asking her active students to re-sign up, but by carrying their existing Stripe customers across so the takeover was invisible to them. Here's how that worked.

The constraint: do not break recurring billing

Active students were already paying her monthly through Stripe. Each had a saved card, a Stripe customer ID (cus_xxx), and a subscription that auto-renewed. Asking them to re-enter card details was off the table — that's the moment customers churn. So the migration plan was:

  1. Pull the customer + subscription roster from WooCommerce's MySQL.
  2. Match each WC subscription to a plan in our system (we already seeded her plan lineup from the WC product catalog).
  3. Create LessonEnrollment records in our system with the original stripeCustomerId attached.
  4. Leave the existing WC Stripe subscriptions running for now — they keep billing on schedule.
  5. When we're confident our system works, send each student a Pay Link from our app. Stripe Checkout reuses the existing customer (no card re-entry), creates a new subscription on our system, and we cancel the WC one.

The ugly part: WooCommerce's data model

WooCommerce stores subscriptions as a custom WordPress post type (shop_subscription) with the actual fields scattered across wp_postmeta. The line items — i.e. which plan the student is on — aren't directly attached to the subscription post; they're on the renewal orders, referenced through a PHP-serialized array in postmeta called _subscription_renewal_order_ids_cache.

That meant the import script had to:

  • Connect to MySQL (we ran the script over an SSH tunnel).
  • For each active or on-hold subscription, fetch its postmeta and pivot it into named fields.
  • Parse the PHP-serialized cache to find renewal order IDs.
  • Look up each renewal order's line items to find the product name.
  • Match that product name fuzzily against our seeded LessonPlan rows.
  • Create User records (using billing email) and LessonEnrollment records, preserving stripeCustomerId.

The unexpected: HPOS vs the old store

WooCommerce 8 has a high-performance order storage table (wp_wc_orders) that's supposed to replace the postmeta system. In this store, both stores existed and they didn't agree — the new HPOS table showed only on-hold subscriptions while the old wp_posts store had the active ones. We ended up trusting wp_posts as the source of truth because that's what was actually billing.

A small handful of subscriptions had naming inconsistencies (one was “1 Group + 1 Private per week” in our system but spelled without the plus sign in WooCommerce). We caught those manually rather than write fuzzier matching — when the universe is a couple dozen rows, manual review beats clever code.

Result

Every active and on-hold enrollment imported with the correct status, and each one carries the original Stripe customer ID. From the trainer's side, every student is visible in the trainer dashboard with full edit / makeup credit / cancel controls. From the student's side, when we send them the new Pay Link they can move to our system in one click without re-entering anything.

The whole import took about an hour to write and ten seconds to run. Going to live is a separate decision — we're testing with a couple of students first before flipping the rest. But the data is there, the customer IDs are preserved, and the trainer gets to delete WooCommerce once she's ready.

More posts
What this platform is, and why we built it
A working barn runs on a stack of sticky notes, group texts, spreadsheets, and a payment processor or two. We replaced all of that with one app — here's what it does.
One login, three views: workers, trainers, and students share an app
A barn employee who also takes lessons shouldn't need two accounts. A trainer who's also a barn manager shouldn't flip between portals. Here's how we model overlapping roles in one user record.
How gamification fixes stall cleaning quality, not just speed
Most barn-work apps that gamify cleaning end up rewarding the fastest, sloppiest worker. Here's the small change to the workflow that makes quality matter as much as speed.