Skip to content

magicpages/ghost-stripe-migration-toolkit

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Stripe Migration Toolkit

A toolkit for migrating paid subscribers from platform-controlled Stripe accounts to your own Stripe account (e.g., Beehiiv → Ghost/Magic Pages).

The Problem

Both Beehiiv and Ghost use Stripe Connect, but with different levels of control:

  • Beehiiv connects with controller.type = "application", meaning the platform controls the account. This prevents other platforms from connecting with the same level of access.
  • Ghost connects with controller.type = "account", meaning you retain control of your Stripe account.

Because Beehiiv claims platform control, you cannot connect the same Stripe account to Ghost. A new Stripe account is required, and subscriptions must be recreated with the new account's Product/Price IDs.

The Solution

This toolkit handles the migration by:

  1. Exporting subscription data from the source account
  2. Copying customer payment methods via Stripe's PAN copy tool
  3. Creating new subscriptions with trial_end set to the old renewal date
  4. Generating a Ghost import CSV with new customer IDs
  5. Cancelling old subscriptions at period end

Result: Subscribers transition seamlessly without double-charging.

Installation

npm install
cp .env.example .env
# Edit .env with your Stripe API keys

Quick Start

1. Configure Environment

Edit .env with your Stripe API keys:

STRIPE_SOURCE_SECRET_KEY=sk_live_xxx  # Beehiiv-connected account
STRIPE_DEST_SECRET_KEY=sk_live_xxx    # Ghost-connected account

Find your API keys in each Stripe Dashboard under Developers → API keys.

2. Export Subscriptions

npm run export

This exports all active and trialing subscriptions from the source account.

Output:

  • output/subscriptions-export.json - Full subscription data
  • output/subscriptions-export.csv - CSV for reference

3. Copy Customer Data

Copy customers (with payment methods) from source to destination using Stripe Dashboard:

  1. Go to source Stripe Dashboard → Customers
  2. Click Copy (top right) → Copy all customers
  3. Enter destination account ID (find it in destination Dashboard under Settings → Business → Account details, starts with acct_)
  4. Go to destination Stripe Dashboard → accept the copy request
  5. Wait for copy to complete (usually minutes, up to 3 days for large datasets)

4. Create Customer Mapping

npm run create-customer-mapping

This matches customers by email between accounts.

Output: output/customer-mapping.json

The summary shows all customers matched. If any are unmatched, ensure the customer copy (step 3) completed.

5. Set Up Ghost Tiers

In Ghost Admin, create your membership tiers:

  1. Go to Settings → Membership → Tiers
  2. Create tiers matching your Beehiiv (or any other platform that follows similar patterns) subscription offerings
  3. Set the prices (monthly/yearly) to match

This creates products and prices in your destination Stripe account.

6. Create Price Mapping

Create config/price-mapping.json to map old prices to new Ghost prices:

  1. Open output/subscriptions-export.json and note the unique priceId values
  2. Go to destination Stripe Dashboard → Products → find the Ghost-created products
  3. Click each product to find its price IDs (under Pricing)
  4. Create the mapping file:
[
  {
    "oldPriceId": "price_xxx",
    "newPriceId": "price_yyy",
    "description": "Monthly"
  },
  {
    "oldPriceId": "price_aaa",
    "newPriceId": "price_bbb",
    "description": "Yearly"
  }
]

Add one entry for each unique price in your export.

7. Create New Subscriptions

npm run create-subscriptions

This runs in dry-run mode by default. Review the output to verify:

  • All customers are mapped correctly
  • All prices are mapped correctly
  • Trial end dates align with old subscription renewal dates

When ready, run with --live to create the subscriptions:

npm run create-subscriptions -- --live

Output: output/migration-results.json

8. Generate Ghost Import CSV

npm run generate-ghost-csv

Output: output/ghost-import.csv

Import this file into Ghost:

  1. Go to Ghost Admin → Members. Click on the gear icon on the top right.
  2. Click Import Members and upload the ghost-import.csv
  3. Verify members appear with correct tiers

9. Cancel Old Subscriptions

npm run cancel-old

This runs in dry-run mode by default. Review the output to verify which subscriptions will be cancelled.

When ready, run with --live to cancel subscriptions at period end:

npm run cancel-old -- --live

Output: output/cancel-results.json

10. Verify Migration

npm run verify

This checks that old subscription end dates align with new subscription trial end dates.

Output: output/verification-results.json

All subscriptions should show as "aligned". If any show issues, review the specific subscription in both Stripe accounts.

Testing

To test this toolkit you can use test Stripe accounts (sk_test_xxx) before running on production.

Set Up Test Data

npm run test:setup

This creates test products, customers, and subscriptions.

Simulate Time Passing

Stripe Test Clocks let you simulate time passing without waiting. This verifies that:

  • Old subscriptions cancel at period end
  • New subscriptions transition from trialing to active billing

Customers must be attached to a test clock when created. Time only advances for customers on that clock.

# List existing test clocks
npm run test:advance-clock -- --list

# Create a new test clock (frozen at current time)
npm run test:advance-clock -- --create

# Advance clock by 30 days (triggers subscription renewals/cancellations)
npm run test:advance-clock -- --clock clock_xxx --days 30

# Check subscription statuses after advancing
npm run test:advance-clock -- --clock clock_xxx --status

Commands

Command Description
npm run export Export subscriptions from source account
npm run create-subscriptions Create new subscriptions in destination
npm run cancel-old Cancel old subscriptions at period end
npm run generate-ghost-csv Generate Ghost import CSV
npm run verify Verify migration alignment
npm run test:setup Set up test data (test accounts only)
npm run test:advance-clock Advance test clock

How It Works

Avoiding Double-Charging

The key is the trial_end parameter:

  1. Old subscription renews on Jan 15
  2. New subscription created with trial_end: Jan 15
  3. Old subscription cancelled at period end (Jan 15)
  4. New subscription trial ends Jan 15, billing begins

Timeline:

Jan 1: New subscription created (trialing)
Jan 1-14: Old subscription still active, new in trial
Jan 15: Old subscription cancels, new subscription activates

Ghost Import

Ghost matches the stripe_customer_id in the CSV to subscriptions in the connected Stripe account. Since we created subscriptions with the correct price IDs, Ghost assigns the correct tier.

Troubleshooting

"No customer mapping found"

Customer wasn't copied to destination account. Use Stripe PAN copy tool.

"No price mapping for price_xxx"

Add the missing price ID to config/price-mapping.json.

"Could not find Stripe customer" in Ghost

The stripe_customer_id in the CSV doesn't exist in the Stripe account connected to Ghost. Ensure:

  1. Customer was copied to the correct account
  2. Ghost is connected to the destination account
  3. Using the NEW customer ID, not the old one

References

License

MIT License - see LICENSE for details.

About

Migrate paid subscribers from platform-controlled Stripe accounts (e.g., Beehiiv) to Ghost

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published