> ## Content Index
> Fetch the complete content index at: https://www.magicpages.co/llms.txt
> Use this file to discover other available public pages before exploring further.

# How we migrated a Ghost ActivityPub account and its 93 followers
- URL: https://www.magicpages.co/blog/migrate-ghost-activitypub-account/
- Published: 2026-11-05T13:00:00.000Z
- Updated: 2026-11-05T13:06:52.000Z
- Description: Part 2 of the pipeline Cathy Sarisky and I built: importing her Ghost ActivityPub account into our own server, and what went wrong on the day.
- Author: Jannis Fedoruk-Betschki
- Tags: Magic Pages Blog

📦

This is part 2 of a two-part series with Cathy Sarisky. In [part 1](https://www.spectralwebservices.com/blog/we-built-a-pipeline-for-moving-between-ghost-activitypub-instances-heres-part-1/?ref=magicpages.co), she explains how she exported her ActivityPub account from Ghost. This post covers how we imported it into Magic Pages.

Cathy writes that "neither one of us is very good at accepting 'no' for an answer". I can't argue with that. She built the tool that gets the data out of a Ghost ActivityPub account, and I built the one that puts it back in. So here's the rest of the story.

On 3 November 2026, Cathy's account, `@cathy@spectralwebservices.com`, went from the ActivityPub service Ghost runs to the one we run at Magic Pages. It kept its address, its 93 followers, and all her posts, likes, reposts and notifications. If she didn't mention it, I bet her followers wouldn't have even noticed.

![](https://www.magicpages.co/content/images/2026/11/image.png)

## Why there's no button for this

Ghost's social web feature, called Network in the Ghost admin, is built on ActivityPub. Ghost(Pro) sites use the ActivityPub service Ghost runs. Self-hosted sites (like Cathy's until yesterday) can use it for free. At Magic Pages we run our own copy of the same open-source server for Magic Pages sites and technically every selfhoster can run their own as well with Ghost's Docker setup:

[GitHub - TryGhost/ghost-dockerContribute to TryGhost/ghost-docker development by creating an account on GitHub.![](https://www.magicpages.co/content/images/icon/favicon-f9a3d06b-4676-457b-903c-7bf2beadf107.png)GitHub![](https://www.magicpages.co/content/images/thumbnail/ghost-docker-ce45753a-09af-4af6-8e52-797c7c25f78b.png)](https://github.com/TryGhost/ghost-docker/?ref=magicpages.co)

While Ghost itself is fairly portable, the ActivityPub service is not. It has no account export and no account import. Cathy's exporter solved the first half of that.

The second half is harder, because an account isn't something you can simply copy. On the fediverse, Cathy's account is a URL:

```
https://www.spectralwebservices.com/.ghost/activitypub/users/index

```

Every server that follows her stores that URL, along with a public key that proves activities really come from her. When Mastodon fetches a profile, it drops it unless the id in the response is exactly the URL it asked for.

The usual way to change servers is an event called `Move`. Mastodon supports it, and since recently Ghost's ActivityPub server does too. You set up a new account somewhere, the old one announces the move, and every follower's server unfollows the old account and follows the new one. But a Move needs two accounts at two different URLs, both online at the same time. Cathy's account is on her own domain, and well...her domain can only point at one server.

So, I thought…whichever server answers for `www.spectralwebservices.com` answers for her account. If our server answers with the same URL, the same followers and the same posts, the rest of the fediverse has no reason to treat it any different.

The one thing our server can't copy is her key. Everything Cathy's account sends out is signed with a private key, and followers' servers check that signature against the public key on her profile. The private key stayed with Ghost's ActivityPub service, so our server signs with a new one and puts the matching public key on her profile. The next time a follower's server sees a signature it can't check, it fetches her profile again, picks up the new key, and carries on.

Tada 🎉

At least in theory.

## What the import did

Cathy's export was a folder of JSON files: her profile, followers, following, outbox, likes, reposts, notifications and the images she'd uploaded. The importer I've built around that writes all of that into our ActivityPub server's database. It uses the server's own code to fetch and store accounts and posts, so it builds itself against the exact version the server runs, and runs in a one-off container next to it. Since the move, I've turned it into one command per step:

```bash
npx github:magicpages/ghost-activitypub-importer prepare  --container activitypub --export ./export --sign-host www.example.com
npx github:magicpages/ghost-activitypub-importer mappings --container activitypub --export ./export --ghost-url https://www.example.com --content-api-key …
npx github:magicpages/ghost-activitypub-importer cutover  --container activitypub --export ./export --commit
npx github:magicpages/ghost-activitypub-importer media    --container activitypub --export ./export --commit
npx github:magicpages/ghost-activitypub-importer objects  --container activitypub --export ./export --commit

```

`prepare` fetches everything the export refers to that our database doesn't have yet. It only adds data, so I could run it while Ghost's service still served the account. Of 25 accounts we hadn't seen before, 18 could still be fetched. The other 7 no longer had ActivityPub at their address: five Ghost sites that had turned off Network, a Mastodon server that no longer answered, and a domain that doesn't exist anymore. Of 274 posts, 236 could be fetched. The other 38 had been deleted, or their servers no longer answered.

`mappings` matches her articles to the posts on her new Ghost site by slug, so that editing a post in Ghost still updates the article her followers saw. It matched all posts that were still online.

`cutover` does the actual move in one database transaction. Without `--commit` it rolls back and prints what it would have done, which is how I tested it.

Our database already knew Cathy, because thirteen Magic Pages sites follow her. `cutover` turns that remote account into a local one, gives it a new key pair, and connects it to her site. Then it restores the rest:

|                     | Imported |
| ------------------- | -------- |
| Followers           | 93 of 99 |
| Following           | 93       |
| Posts in her outbox | 421      |
| Likes               | 245      |
| Reposts             | 69       |
| Notifications       | 984      |

The six missing followers, and the one missing account she followed, are those seven accounts that couldn't be fetched.

Because of the new key, Mastodon servers send the follow again for each of their users who follow her. For Cathy that meant...a bunch of new follow notifications that weren't actually new 😄

`media` copies the 31 images from the export into our storage and points her posts at the new URLs. The last command, `objects`, didn't exist during the migration. I wrote it after something broke, which is further down.

## The order on the day

Since two servers can't answer for the same account at once, so we did it in this order:

1. I ran the cutover with `--commit`. Nothing changed publicly, because `spectralwebservices.com` still had its DNS pointed at her old server.
2. Cathy turned off Network on her old Ghost install. While it's on, Ghost's ActivityPub service treats her as its own account and signs anything it sends with the old key, which no longer matches the profile her followers would fetch. Turning the Network feature off meant that Ghost's server thinks "Well...no more Activitypub for Cathy". About a minute later, Ghost's service stopped serving her account.
3. Cathy switched her DNS to Magic Pages.
4. A few moments later, her profile, the follower and following counts, her outbox, a few posts, and that a lookup for `@cathy@spectralwebservices.com` found the new server. Success!

## What went wrong

After the DNS switch, her Ghost site was briefly set up on `spectralwebservices.com` without the www. That was human error and would have changed every URL in her account. Every follower's server knows her by the www URL, so an account on the root domain is a different account, and the follows wouldn't carry over. When we noticed the whoopsie, Cathy quickly set the site back to www.

Shortly after the switch, I tried to like one of Cathy's notes from our own Magic Pages account. It failed with `Object to like not found`.

Our server keeps two things for every post. One is a row in its database, which is what the Ghost admin shows. The other is the post as ActivityPub data, the JSON other servers get when they open the post's URL. For posts from other servers, it only keeps the row, because their own server answers for them.

The import saved the ActivityPub data for everything in the export's outbox. But 112 of her notes weren't in it: 110 replies and two other notes, one of them written the night before the switch. As Cathy explains in part 1, Ghost's API never lists your replies directly, so the exporter has to find them another way.

The good thing: we already had those notes, because lots of Ghost sites on Magic Pages follow her. But we had them the way we have any post from another server, as a row without the ActivityPub data. After the move, other servers asked our server for them, and it answered with a 404\. So I added an extra command, `objects`, that builds the ActivityPub data from the row, and the like went through.

So if you run the receiving server for a move like this, make the export as close to the switch as you can, and run `objects` afterwards. It finds the account's posts that only have a row and builds the missing ActivityPub data.

## What this doesn't solve yet

The importer needs access to the Docker host of the ActivityPub server, so it's a tool for whoever runs that server: us, for Magic Pages sites, or you, if you run Ghost's ActivityPub server yourself. So, treat this as a guide for selfhosters.

Cathy's exporter and my importer are both on GitHub, under the MIT licence:

[GitHub - cathysarisky/ghost-activitypub-exporter: Export a Ghost site's ActivityPub data (posts, notes, followers, replies, media) from ap.ghost.org as a raw JSON archiveExport a Ghost site's ActivityPub data (posts, notes, followers, replies, media) from ap.ghost.org as a raw JSON archive - cathysarisky/ghost-activitypub-exporter![](https://www.magicpages.co/content/images/icon/favicon-5743e950-01d3-4bb0-a021-a154f6c1f371.png)GitHub![](https://opengraph.githubassets.com/cedbdb0f98ad47d3e42cbc77e106b2551f28363f9cde9d8417e5e15840f2b767/cathysarisky/ghost-activitypub-exporter)](https://github.com/cathysarisky/ghost-activitypub-exporter?ref=magicpages.co)

[GitHub - magicpages/ghost-activitypub-importer: Import a Ghost ActivityPub account export into a self-hosted TryGhost/ActivityPub server, keeping its followersImport a Ghost ActivityPub account export into a self-hosted TryGhost/ActivityPub server, keeping its followers![](https://www.magicpages.co/content/images/icon/favicon-74a3917a-2d05-4500-aa0c-8eaf46eccee1.png)GitHub![](https://www.magicpages.co/content/images/thumbnail/ghost-activitypub-importer-6aa8205b-0788-42ec-a80d-55570f080eb4.png)](https://github.com/magicpages/ghost-activitypub-importer?ref=magicpages.co)