Migrating a Discourse forum to atmoBB is not a normal data migration

Someone asked recently whether an existing Atmosphere community could move from Discourse to atmoBB. My first thought was that it's a converter project: export users, categories, topics, and posts from Discourse, turn them into atmoBB records, import them, done. And, if that were it, it would be so simple!!

A regular forum owns the database its users and posts live in. An atmoBB forum doesn't. Public topics and replies live in their authors' AT Protocol repositories. The forum is an AT Protocol account too, but its repository only holds the forum profile, categories, boards, staff grants, and moderation actions, and everybody's actual conversations live somewhere else.

So moving a Discourse community over isn't one admin copying data from one database into another. It's a coordinated migration that involves every author, their identity, their repository, and their consent, and I don't have good answers for most of it yet 😅

Who gets to create an old post?

In atmoBB, a public topic is a record in its author's repository, and a reply is a record in the replier's repository. Writing either one needs authorization from that account.

A Discourse admin has authority over the Discourse database. That doesn't give them any authority over a member's AT Protocol repository. Having a Discourse backup with Alice's old posts in it doesn't let the importer publish them as Alice.

Publishing every imported post from the forum account would be way simpler technically, but it throws away one of the main points of atmoBB. The forum would own the repository while the page says a member wrote the post, and the member couldn't take the post with them when they move PDSes, edit it as their own, or delete it from their repository. I wouldn't really call that a migration.

The clean ownership model has each former Discourse member authorize atmoBB to write their own topics and replies. Which opens up a whole can of worms:

  • What happens to posts by people who never make an AT Protocol account?
  • What about members who have died, left the community, lost access, or just don't respond?
  • Is the migration complete if the most important topic starter never shows up?
  • How long does the migration stay open for later claims?
  • Can someone authorize their posts without joining the new forum?
  • Can they look through their posts and exclude some before publishing?
  • What if they authorize an import and later change their mind?

There's no transaction that spans hundreds or thousands of independent repositories, so a migration could just sit there half done forever.

Usernames aren't identities

Discourse knows a user by an internal numeric ID and shows a username. AT Protocol knows an account by a stable DID and currently shows it through a handle that can change, and none of those line up with each other.

A mapping typed in by the forum admin doesn't prove that whoever controls @alice on Discourse also controls alice.example on AT Protocol. Handles change, old usernames get renamed or reused, and admins make honest mistakes.

Members would probably need to claim their old Discourse identity:

  1. The migration stores the immutable Discourse user ID and its current username.
  2. The old forum sends a one-time claim link through a channel that Discourse account controls, like a private message.
  3. The member follows the link and signs into atmoBB with their AT Protocol account.
  4. The migration shows exactly what will be made public, then binds that Discourse user ID to the account's DID.

Even that leaves questions:

  • Does the old Discourse instance have to stay online and able to send private messages for the whole claim period?
  • Is email an okay fallback? If so, should email addresses be copied into the migration system at all?
  • How do deleted, anonymized, staged, suspended, and system users get represented?
  • Can one AT Protocol account claim several old Discourse accounts?
  • What if one Discourse account needs to be split between multiple people?
  • How do you reverse a fraudulent or mistaken claim after public records are already published?
  • Should claims expire, and who can reissue one?

For a while, the migration service would be holding a really sensitive link between private Discourse account data and public decentralized identities. Protecting that data, and eventually deleting it, has to be part of the design from day one.

Order matters

An atmoBB board record lives in the forum's repository. A topic points to that board. Every reply has a strong reference to its topic, meaning both the topic's AT URI and its content identifier (CID). A reply can also strongly reference another reply as its parent.

So the import has to happen in order:

  1. The forum authorizes creating categories and boards.
  2. The topic starter claims their identity and publishes the topic.
  3. The importer gets back the resulting URI and CID.
  4. Other members can publish replies to it.
  5. Nested replies may add more ordering constraints on top of that.

If a topic starter never claims their account, every reply in that topic is stuck, even replies from people who did claim. You could import the replies as disconnected posts, but then the conversation is gone, and you can't hand the topic to someone else without lying about who wrote it.

A reliable importer would also need deterministic record keys, resumable jobs, dependency tracking, idempotent retries, and a durable audit trail, because retrying after a failure can't duplicate half a forum. That stuff is all solvable! It just does nothing for the missing-author problem.

Two different times

An imported record can carry the original Discourse createdAt, but the record is being committed to an AT Protocol repository today, and consumers could pretty reasonably read those two times differently.

That matters for more than display order:

  • Should importing ten-year-old posts show up as new activity on the network?
  • Should it fire notifications, watched-board alerts, or mention alerts?
  • Do imported posts count toward first-post stamps and other historical forum state?
  • What about old posts that predate the member's atmoBB profile or forum membership record?
  • On a gated forum, were these posts written inside a membership window that didn't exist yet?
  • How does an appview tell a historical import apart from a new post with a forged createdAt?

Adding an importer-only marker to normal discussion records would need a protocol decision. Trusting a forum to vouch for everybody's historical content would also create a new authority relationship that atmoBB doesn't have right now.

The source keeps moving

A Discourse export is a snapshot, and members can keep creating, editing, moving, and deleting content after it's taken.

So a cutover needs an explicit consistency plan. You could put Discourse in read-only mode before the final export, do an initial copy followed by a delta pass, or try a period of dual writing, and every one of those costs something:

  • A long claim period fights with a short maintenance window.
  • A topic might get claimed before its latest edit makes it into the export.
  • Someone might delete a post on Discourse after approving an earlier copy.
  • IDs and references have to stay stable across repeated exports.
  • New replies can show up under a topic that's already been imported.
  • There's no atomic moment where every independent PDS switches over together.

The practical cutover might need to separate claiming identity from publishing records: collect claims ahead of time, freeze Discourse, then publish a final export for everyone who claimed. That's still a whole community operation and not an import command you run once.

The long tail of Discourse content

Basic paragraphs, headings, links, lists, quotes, and code blocks can be converted. The long tail is harder:

  • Discourse Markdown, cooked HTML, BBCode accepted by plugins, and custom theme markup
  • mentions of Discourse usernames that may or may not have claimed DIDs
  • quotes of posts whose authors or parent topics are still unclaimed
  • local emoji, oneboxes, link previews, footnotes, tables, and plugin-defined blocks
  • polls and votes
  • tags, accepted answers, wiki posts, bookmarks, reactions, and likes
  • post revisions and edit history
  • deleted, hidden, flagged, or staff-only posts
  • topic timers, slow mode, auto-close state, and other Discourse-specific behavior

For each feature, the migration has to pick: keep the semantics, keep only readable text, link back to the old site, or drop the data. I'd want those choices in a preflight report, not discovered after cutover.

Uploads

Images in public atmoBB posts are blobs in the author's PDS, referenced by a record in that same repository. You can't just copy a forum-wide media directory into one central atmoBB upload directory.

To migrate an embedded image as a real atmoBB image, the importer needs the author's authorization to upload the blob to their PDS and put its blob reference in their post record. That drags in storage quotas, file-size and media-type limits, duplicate files, broken source uploads, remote hotlinks, and PDS rate limits.

Leaving every image on the old Discourse domain gets around the authorization problem, but then the migration depends on the old server forever, and the day the old site goes off the archive breaks. Copying all the media to a separate web host keeps things rendering but loses member ownership.

Avatars have their own identity question. A Discourse avatar is forum account data. An atmoBB avatar is part of the member's AT Protocol profile and follows them to every forum, so a forum migration shouldn't overwrite someone's existing cross-forum identity without them explicitly saying yes.

Accounts don't migrate

Discourse passwords, sessions, two-factor credentials, email verification, trust levels, and SSO relationships can't turn into AT Protocol accounts. Members need an existing account or have to create one through an account provider.

Profile fields have a different scope too. A Discourse bio, avatar, title, website, and signature came from one forum. Some atmoBB profile fields are account-wide and others can be overridden per forum. Importing a forum bio as an account-wide identity could change how that person shows up on every atmoBB forum without them expecting it.

The migration has to decide which profile data gets offered as an opt-in forum override, which gets ignored, and which can safely become an account default, and none of that should happen silently.

Private categories

Public Discourse categories map pretty well to atmoBB categories and boards. Private and restricted areas don't.

Discourse can restrict categories by group, trust level, staff status, and plugin-defined policy. atmoBB's members-only boards use Happyview permissioned spaces. Those topics and replies don't live in public AT Protocol repositories at all. They live in the forum's appview database, which has different backup and portability properties.

Open questions:

  • How do Discourse groups become space memberships?
  • Who authorizes those memberships, and at what point in the cutover?
  • Can private content be staged without exposing it to the public firehose?
  • Do former members get access back to an old private category?
  • What happens to private content written by someone who never claims an account?
  • Can the operator prove a supposedly private export never got written to a public repository?

Personal messages and group messages are a separate product, and they aren't forum topics. atmoBB doesn't have a migration destination for them right now, so they should be explicitly excluded before they accidentally get imported as public discussions (which would be a very bad day).

Moderation history

Discourse moderation state includes staff roles, category moderators, suspensions, silences, deleted posts, flags, warnings, closed topics, pinned topics, and a staff action log. atmoBB represents moderation as records signed by the forum account.

Replaying old actions as if the new forum account did them years ago would create a public audit history that never happened on AT Protocol. Dropping all of it loses useful state. Copying only the current state (who's staff, which topics are locked, which members are still banned) might be the least misleading option, but even that needs a review.

The policies don't line up either. A Discourse suspension could cover private messages and login as well as posting, while an atmoBB ban affects forum writes and indexing. Trust levels, badges, likes, and flags have no direct equivalent. Someone has to sort out which state can be translated and which history can only be archived.

Membership

On an open atmoBB forum, a membership declaration lives in the member's repository. On a gated forum, membership also needs an acceptance authored by the forum. A Discourse user row is neither of those.

Declaring membership automatically for every exported user runs into the same authorization problem as importing posts. Auto-accepting everybody might be reasonable for a gated successor forum, but that still doesn't create the members' own declarations. People who only read, people who posted once years ago, suspended users, and people who opted out of the migration each need a deliberate answer.

Arrival dates, sponsors, invitations, applications, first posts, and stamps can pick up false historical meaning if they're rebuilt mechanically. I don't want the migration making up a tidy history for data that never had one.

Links and redirects

Discourse URLs identify topics and posts by Discourse IDs and slugs. atmoBB URLs identify records by DID and record key. Every internal link, external bookmark, search result, email archive, and quoted URL expects the old address to keep working.

A serious migration needs a permanent redirect map for topics and individual posts. That map can't be final until the matching atmoBB records exist, and those might depend on claims that never come in, so links to unclaimed content need somewhere to land.

The old domain might also host uploads, API endpoints, feeds, and well-known URLs. Reusing it for the atmoBB forum while keeping a legacy archive and redirects means planning the routing on purpose. Search indexing, canonical URLs, and link previews shouldn't present duplicate copies as separate conversations.

Federation

A post on a Discourse forum went out under that forum's access rules and social expectations. A public atmoBB post is a record in the author's repository that any compatible appview can index, and boards can also take part in topic federation across forums.

Even if the old Discourse category was public on the web, members might not expect years of posts to turn into portable repository records tied to their DID. The claim screen has to explain that change in audience clearly, because "we moved the website" isn't consent.

No imported board should get opted into topic federation just because its name looks like an existing topic. Federation is a new distribution decision for the forum owner and its members.

Rollback

Before anything is published, staged migration data can be deleted like any private import job. Once records are written to members' repositories and announced on the firehose, restoring the forum database doesn't pull them back.

The importer could delete records from accounts it still has authorization for, but other appviews may have already indexed them, the authorization may have expired, and some users may have edited things or moved their accounts in the meantime. A failed migration can't promise a clean global rollback.

That's why preflight, dry runs, explicit consent, small batches, and audit logs matter way more here than for a centralized database import. "Take a backup first" is still necessary for the forum's own state, but it does nothing for records already out on the network.

Privacy and deletion

A full Discourse backup can include email addresses, IP-derived metadata, deleted content, private messages, staff notes, and stuff only restricted groups could see. Most of that isn't needed to convert public forum content and should never get into the migration system.

The process needs written answers for:

  • which source tables and API fields get collected
  • where the export and claim mappings are stored
  • who can look at them
  • how they're encrypted and backed up
  • when raw exports, claim tokens, and identity mappings get deleted
  • how data subject requests are handled during and after the migration
  • what evidence gets kept to show a member approved publishing

Because AT Protocol publishing is decentralized, "delete" can't mean every indexer forgets a record. Members need to understand that before they import years of history under a durable DID.

The options

I see four broad approaches right now, and every one of them gives something up.

1. Claim and publish

Members prove they control their Discourse identities, connect AT Protocol accounts, and authorize their own records. This keeps atmoBB's ownership model intact better than anything else.

It also guarantees an incomplete migration unless nearly everybody participates. Unclaimed topic starters block repliers who'd otherwise be happy to move, media imports need per-user writes, and the whole thing could take months.

2. A forum-owned legacy archive inside atmoBB

The forum imports old content into a new archive record type and shows the original Discourse username as legacy attribution. Claims could link identities later, but the records stay forum-owned unless they get republished.

This keeps history readable and the conversations intact, but it needs new protocol records, appview queries, UI, moderation rules, and very obvious ownership labels. It also creates a permanent second kind of topic in atmoBB and weakens the simple rule that public posts belong to their authors.

3. A static read-only Discourse archive next to atmoBB

The old forum becomes a frozen archive. The community starts new conversations on atmoBB, with redirects or links between the two sites.

This is honest operationally and keeps old URLs, authorship, and uploads. It isn't a data migration, though. Old discussions don't become atmoBB records, don't show up naturally in member activity, and still depend on someone hosting the archive.

4. A clean start

The forum brings over its categories, rules, staff, and current membership policy, but not the old discussions. Authors can link, summarize, or repost important topics by hand.

This loses the most continuity but has the clearest story on ownership and consent. For some communities, keeping the old site up and starting fresh is probably better than a technically impressive migration nobody really agreed to.

What I'd want answered first

Before writing any code, I'd want firm answers to at least these:

  1. When complete history, correct member ownership, and a practical community cutover conflict, which one wins?
  2. What's the permanent representation of unclaimed topics and replies?
  3. How does a member prove a Discourse identity, and how long can they claim it?
  4. What source content is excluded outright: private messages, deleted posts, restricted categories, revisions, moderation notes?
  5. Where do uploads live when their authors don't claim them?
  6. Which current moderation and membership state transfers, and which history stays an archive?
  7. How do old URLs keep working permanently?
  8. What's the freeze and cutover procedure?
  9. What does failure recovery mean once records have hit the network?
  10. What exact consent text explains public repository ownership and decentralized indexing?
  11. When is the migration finished if claims are still outstanding?

So

The Discourse parser is the easy part! The hard part is deciding who owns a community's history and what it means to republish it on a decentralized network...... and that feels really complex an so "clean start" or "static archive" feels easiest, but the least fulfilling as a community member :(

Hi, I'm Keith! You can @ me with any bugs or problems or ideas, but pls be nice.

Someone asked recently whether an existing Atmosphere community could move from Discourse to atmoBB.

That'd be me 👋

Thanks for this! I've invited a handful of other atproto group comms people in here so we can think through this case collectively.

A lot of the complication comes from atmobb's ownership model, which strictly considers posts the sole ownership of users. Roomy follows more of a 'mutual ownership' model between community & members, which I think makes this kind of migration look quite different; meri and zick can speak more to that.

For my part I'll try to resolve a few considerations, scoping the work specifically to a prospective migration of discourse.atmosphere.community to a new site like forum.atmosphere.community hosted on atmobb.

---

A reliable importer

There seems to be an presumption baked into this assessment that the importer ought to work seamlessly as a self-serve utility. That is a fine long term goal, but in reality every forum migration is a bespoke undertaking that inevitably requires some amount of hand-editing to smooth out the bumps along the way.

For reference:

https://meta.discourse.org/t/migrate-a-vbulletin-4-forum-to-discourse/54881

https://meta.discourse.org/t/migrate-an-smf2-forum-to-discourse/90314

The source keeps moving A Discourse export is a snapshot, and members can keep creating, editing, moving, and deleting content after it's taken.

This is a non-issue with proper order of operations:

  1. Put discourse in read-only mode.
  2. Export & import data
  3. Perform redirects

The migration should be a one-off, not an ongoing sync (aka a bridge).

The long tail of Discourse content Basic paragraphs, headings, links, lists, quotes, and code blocks can be converted. The long tail is harder

Stuff like revisions is way out of scope for a migration, and the atmosphere discourse has scarcely made use of any advanced Discourse features. What can't be fixed by hand will be a minor glitch in 1/100 threads, which is perfectly acceptable. A migration isn't expected to be lossless.

Me and @meri.garden have discussed this a lot and I think we came to something that's a good balance of the pros and cons. I'll walk through how we would go about it hypothetically for AtmoBB based on our plans for Roomy.

First-class support for "bridging"

An important first step having AtmoBB data model ( lexicons ) support "bridging" or "masquerading" natively. For example, there needs to be a way for the forum to designate trusted "bridge" accounts that are are allowed to "masquerade" and make topics and replies that are attributed to a different account.

This means that topics and replies need to be allowed to have an attributed author DID as a field, and that should be in the lexicon.

If I am not an account that has been configured by the forum as allowed to masquerade, then any topic or reply that I send where I set an author that is not my own DID will be ignored by the AppView and either not indexed, or marked hidden in case the admin later configures the account as allowed to masquerade.

This feels a little bit inelegant, but I think it's actually the most honest setup. It clearly shows on protocol that the admin has configured certain accounts that are able to pretend to be multiple users. These posts can be marked by the application as being "masqueraded", too. Nothing is hidden, and we don't have to pretend that a bunch of users originally posted these discussions on protocol when they really didn't. Also, other than the extra author field, nothing else about the data model needs to change.

In my opinion it's impractical to coordinate all of the original authors of the Discord posts to login and get the system to migrate their old posts under their own accounts. Also, Discourse import is something that you would want to be able to work for any Discourse forum, not just ones where everybody has an ATProto account tied to their Discourse account.

Also it adds the ability to do bridging with external systems, which can be very handy. I've seen how Railway's support forum was bridged to Discord and it was pretty slick.

Now Importing is Straight-Forward Again

Once you have bridging, an import from Discourse is easy again! You can take a Discourse data dump, process it as much as you need to, and all the imported topics and replies can be written to a single ATProto account that you control. You have that account join the forum, and you give it permission to masquerade and you're set!

What DIDs to use for imported identities

There is a question of what DIDs to use for the authors of the imported messages. For example, in Roomy, when bridging from Discord, we literally use an ID like did:discord:329659531396070482. It unambiguously represents a Discord user ID that the appserver can use to link to a Discord profile and avatar.

If you are importing from Discourse, though, you probably want to link to an imported user profile record that can be stored on the bridge account, so that you don't have a long-term dependency on the Discourse service or its profile information.

Using an at:// URI for the author instead of a DID might be a way to approach that. For example at://did:plc:bridge-account/app.atomobb.profile/imported-profile1.

So a decision need to be made here, the requirement is just that we have a unique URI ( and DIDs are URIs ) that we use as the imported author, and the AppView just wants to be able to resolve that to some kind of profile that it can use to display for who originally posted the message.

( Not ) Having Users "Claim" Their Imported Posts

I honestly think it's not worth the effort to try and have users claim their posts after they are imported. They were not originally written to the user's PDS and, especially with the strong links used for replies, there is not a great way to put them on the user's PDS now without breaking the links between replies.

Having the imported history of the forum be owned by the bridge account and blessed by the forum owner is enough I think.

If you do have a reliable mapping between Discourse users and ATProto accounts, then you could specify the original user's DIDs as the authors of the imported posts, and the forum would attribute all the posts correctly, even though they aren't in the PDS since the bridge is trusted.

You could argue that this is trusting the forum owner too much because it is allowed to show posts under a user's account that isn't actually on their PDS, but any app or AppView could do that anyway, and in this case that is exactly what I think is proper. Again, you can mark posts that were imported different so it's clear that there is a difference in provenance.

But really, this is going to be only the older posts, it's somewhat expected that an import could have rough edges, and on protocol, everything is transparent and the trust lines are clear:

  • The forum owner trusts the bridge account
  • The bridge account makes claims about who did what according to the imported data
  • The app can show which messages were imported and be clear about the fact that the forum admin is responsible for the integrity of those imported messages
  • If we posses a import user → ATProto DID mapping then users are attributed for posts they wrote before the import

Anti-abuse Mechanism

As I'm writing this I did have one thought about an abuse vector: I could create a forum and trust a bridge account, then fabricate a whole history of topics from different ATProto user accounts on my forum, and then try sharing links to these forum topics to try and trick people regarding what other people said.

To reduce the risk of that, the AppView could much more aggressively mark topics / replies that were masqueraded and attributed to users that have not joined the forum yet. That way, if you "import" a topic by me into your forum and I've never even heard of your forum before, your imported topic will be very apparently marked as not trusted or official.

But if I join your forum, and I stay a member, then the imported messages become less aggressively marked, because I'm implicitly demonstrating some trust in your forum by being a member. If I leave, those message go back to being marked as I am retracting my membership.

Anyway, I hope that adds some useful perspective!

@erlend.sh 🙇‍♂️ Thank you for pushing me!

@zicklag.dev Ohhhh, I hadn't considered bridging. I really struggled with how to get it so that everyone didn't need to transfer an account, but then you'd have some kind of half history with ghost posts. Going to think about this!

Hi, I'm Keith! You can @ me with any bugs or problems or ideas, but pls be nice.

To be clear it’s the masquerading accounts (Discourse called them sock puppets) that makes bridging possible, and you only need the former to simplify imports. Thinking through the bridging case as a whole makes this a lot easier to grok, I just wanna make sure the scope remains constrained to a one-off import rather than a continuous sync.

So all this being said, imo the only good option is #2, “A forum-owned legacy archive inside atmoBB”.

have something to add?

Jump into the conversation.

Already use Bluesky, Leaflet, or another app on the network? You already have an atmosphere account. Log in with it here to add your reply—there's no separate forum account to create.

What's an atmosphere account?

It's an account that works across Bluesky, Leaflet, and other apps on the same network. You can use that account here too.

some apps on the network
Bluesky Leaflet Surf Spark pckt PDSls plyr.fm Tangled BookHive Grain
create an account on Bluesky →