A platform migration is a mapping problem. Users have to become users. Posts have to become posts. Attachments have to land on disk. Permissions have to become someone’s access model. Old URLs have to 301. Everything else — the credits shop, the gallery, the page-node FAQ — is either a rebuild or a eulogy. If you start from a feature matrix, you will import the easy four and discover the fifth on Monday.
This is the general runbook: what to inventory, what official importers actually point at, and how the five objects move. It is not the should you migrate decision and not the stay-or-leave XenForo filter. Those two decide whether. This page assumes you named a destination. It is also not the phpBB → XenForo cookbook, not the XenForo → Discourse dry-run, and not the XenForo host-move backup. Those are the product-specific paths. Use this to know which path you are on.
Official XenForo importing: docs.xenforo.com/manual/importing/. Official direction, said once: XenForo’s Importers add-on lists Discourse 3.x as a source into XenForo. The other way is Discourse’s script/import_scripts/xenforo.rb. XenForo does not ship an export-to-Discourse tool.
First: name the job
People say “migration” and mean three tickets. The XF backup article already split them. Repeat it so you do not run an importer to move hosts.
| Job | What moves | Success |
|---|---|---|
| Backup / restore | Same product, same version, DB + files | Members and uploads return |
| Clone / host move | Same product, then URL and cookies | Login survives a refresh on the new hostname |
| Import | Users / posts / … from another product (or another site on the same product) | New IDs or retained IDs; old URLs need 301s |
This article is the third row. If you are moving XenForo to a new VPS, stop. Take the engine-agnostic backup inventory and restore onto the same version.
An upgrade is a fourth job. Official XenForo: you cannot downgrade. Discourse rebuilds are not imports. Do not mix “we are also upgrading” into cutover night if you can help it.
Direction of the official tools
At the time of writing, XenForo 2’s separate Importers add-on (Customer area, not in the default zip) lists sources into XenForo:
- XenForo 2.x
- vBulletin 3.6 to 5.3
- IPS 4.x
- phpBB 3.2 to 3.3
- SMF 2.0
- MyBB 1.8
- Discourse 3.x
Read the Discourse line twice. Official XenForo will ingest Discourse 3.x. It will not emit Discourse.
The other popular direction:
- XenForo → Discourse: Discourse-side
xenforo.rbagainst a local copy of the XF MySQL database. Five stock steps: users, categories, posts, private messages, likes. - phpBB → XenForo: official importer, phpBB 3.2–3.3 only. Details: the phpBB article.
- XenForo → XenForo: official source. Use it to absorb a second board, not to move hosts.
- Flarum / NodeBB → anything: unofficial or missing. Do not invent a first-party importer the manuals do not list.
- Anything → phpBB / Flarum / NodeBB: treat as a project with a community script, not as a button.
If your pair is not on this list, you do not have an official night. You have a contractor, a CSV of users, and a freeze.
Speed (official XF): do not import over the WAN from production into production. Network latency on thousands of queries is a “massive slow-down.” Snapshot source DB + files, import on a fast box with both databases local, then deploy the result.
The five objects you must map
Everything you care about is one of these, or it is a rebuild.
1. Users
| Question | Why it matters |
|---|---|
| What is the unique key? | Email is the merge key almost every official importer offers. Username is a second, dangerous key. |
| What happens to passwords? | Hashes may come across inside one ecosystem (phpBB → XF is built for it). XF → Discourse: do not assume hashes survive. Plan a reset or a new identity provider unless the current script docs say otherwise. |
| Who is staff? | XF is_admin / is_moderator versus Discourse admin/mod flags versus phpBB founder. Spot-check humans, not counts. |
| What is skipped? | Stock xenforo.rb skips banned / invalid XF users. If you need a ban archive, export it yourself. |
| Avatars? | Files on disk, not only a column. Wrong path = successful import, empty faces. |
Official XF user merge, when the importer creates users: same email — usually yes. Same username — only if you know Admin is the same person. Merge mistakes attach 40,000 posts to the wrong human.
On a fresh empty XenForo, your install admin’s email may match the source founder. Merge-by-email then folds them — usually what you want.
SSO, connected accounts, and two-step secrets are not “users.” They are a week of re-linking after cutover. Tell members.
2. Posts (and the rooms they live in)
| Source idea | XenForo | Discourse | What breaks |
|---|---|---|---|
| Room | Node (forum) | Category (one per topic) | Deep XF trees flatten; Discourse depth > 2 becomes tags on the nearest subcategory in stock xenforo.rb |
| Thread | Thread | Topic | Hidden / deleted / unapproved: do not assume they travel |
| Reply | Post | Post | BBCode → Markdown on the way to Discourse ([color] stripped) |
| Prefix | One prefix | Tag or lost | Stock xenforo.rb does not treat prefixes as a first-class tag group |
| Custom thread field | Field | Lost unless you write a map | Listings, applications, structured data |
| Page node | Page | Nothing | Rebuild as a topic, docs plugin, or external site |
Count visible threads and posts on the source before you run. Count again on the destination. A 40% miss is a filter or a path, not “the new software is lighter.”
Do not pre-create a parallel museum on the destination and then import. You will get two Generals. Import, then merge rooms.
3. Attachments (and avatars)
Attachments are files plus rows. Importers that only see the database produce posts with holes.
| Product | Where the files usually live | What the importer wants |
|---|---|---|
| XenForo | data/ (public), internal_data/ (often raw attachments) |
Same-version restore if you are not importing; for XF→Discourse, ATTACHMENT_DIR / AVATAR_DIR paths the script documents |
| phpBB | files/ (and avatar paths you may have customized) |
Full filesystem path on the import box |
| Discourse | Uploads local or S3 | Discourse backup already bundles them if backup_with_uploads was on |
| vBulletin-class | On disk; official notes exist | Full system path. Guess-and-check is how you import a board with no files |
Official XF: some importers auto-fill paths if XF sits in the directory the source used to occupy. If you installed on a new vhost, you will type the paths. Have them ready.
After import: open ten ugly posts (images, nested quotes, a file that is the post). Zero-byte attachments are a path bug. Restore the destination from backup and run again. Do not “re-run only avatars” unless the official runner you are on actually offers that step after Complete — XF’s run chapter does not treat that as a casual second pass.
4. Permissions
Permissions almost never survive as permissions. They survive as a homework list.
| Source | Destination reality |
|---|---|
| XenForo groups + node overlay | Discourse categories + trust levels + groups. Never/Yes/No does not exist. Deep private trees become a policy rewrite. |
| Discourse categories + TL | XenForo groups + node tree. You will invent groups for things TL used to automate. |
| phpBB groups + forum ACL | XenForo groups. Tidy after. The four XF default groups stay. |
| “Private” rooms | Re-test as a guest, a regular, and a member who should be denied. |
Do not clone an 80-node overlay onto Discourse. Community XF math still says permission rebuild cost scales with groups × nodes; a post-import tidy that also creates groups and nodes can lock a large XF tree. Off-peak.
Staff: confirm admin/mod flags on named humans before you announce. A successful post count with no working moderator is a failed cutover.
5. Permalinks
This is the SEO project. It is also the member-bookmark project.
| Direction | What you get | What you must build |
|---|---|---|
| Into XenForo | An import log table (official suggestion import_log) mapping old ID → new ID. Retain content IDs only on a XenForo with no threads and no posts. Default is new sequential IDs — official: faster and more reliable in most cases. Redirect scripts sit where the old software lived and query that log. |
301s for every public shape you ever served (viewtopic.php, /threads/title.id/, /index.php?…) |
| XenForo → Discourse | Stock script writes Discourse permalinks board/{node_name} and threads/{thread_id} — not /threads/title.12345/ |
Edge rules that extract the XF id and send it to /threads/{id}, then let Discourse finish the hop to /t/slug/id |
| Same product, new host | URLs can stay if the rewrite engine is set up | Friendly URLs 404 is a vhost problem, not an import |
Retain IDs are not required for redirects. The log is. Do not tick retain IDs on a board that already has introductions and a staff room. You will collide. Do not empty xf_thread by hand to “make it empty.” Reinstall.
XFRM, XFMG, and page nodes have no stock permalink on the way to Discourse. 301 them to one honest hub topic, or a 404 with search. A silent 200 on an empty category is worse.
Inventory before anyone runs a script
Write this on one sheet. The importer will not.
- Pair and direction (phpBB 3.3 → XF 2.3; XF → Discourse; Discourse 3.x → XF; other).
- Official tool or “no official tool.”
- Counts: users (valid / banned), rooms, visible threads, visible posts, PMs, attachments, avatars.
- Graveyard: resources, galleries, page nodes, custom fields, credits, prefixes that are load-bearing, SSO.
- URL shapes you actually serve today (log a week of 200s, not your memory).
- Staff who can log in on night one, in the destination’s mental model.
- Backup of the destination — the thing you will destroy if the import is wrong. Source is usually not modified (official XF: source DB and files are never modified).
- Freeze plan: snapshot, import the snapshot, freeze the source when you cut. Do not race live posts.
If line 4 is the product, stop and re-read the decision essay. A mapping runbook will not resurrect XFRM on Discourse.
Architecture of a safe import (any pair)
The shape is the same even when the binary changes.
- Snapshot the source. Database + the file class that holds uploads. Leave production up until the final cut if you are importing the snapshot.
- Stand up a working box that is not public. Both databases local if the importer talks SQL.
- Install a clean destination (or restore a clean backup). If you need retain IDs on XF, this destination must have no threads and no posts. Creating two test threads to “see if it works” disqualifies retain IDs.
- Install the importer (XF: Customer-area add-on so Tools → Import data lists sources; Discourse: gems
mysql2andphp-serialize, script inside the app). - Configure steps. Official XF: once started, you cannot change the step set. Dependencies exist (posts need threads need forums).
- Run. XF: browser or
php cmd.php xf:importfrom the directory that containscmd.php— CLI is significantly faster; both are resumable. Discourse:RAILS_ENV=production bundle exec ruby script/import_scripts/xenforo.rb. - Rebuild what the product tells you to rebuild (XF: Complete import caches; search if instructed).
- Walk the five objects. Users, ten ugly threads, attachments, a denied room, twenty old URLs.
- Take a destination backup. Restore that onto production. Do not keep using the import droplet as prod unless you planned its hostname and TLS from day one.
- 301s, then DNS. Keep the old box as the rollback until you have a day of logs.
Hosted Discourse can be the destination; official pricing has listed migration services on the Business plan (re-check discourse.org/pricing). Hosted can ingest a backup you built on a self-hosted import box. Data belongs to the customer.
Permalinks: a design you can test on staging
Build rules against the staging destination that already has the log or permalink table. Curl twenty known-good old URLs. Curl twenty you expect to miss.
A practical design:
- Canonical thread/topic URLs. Extract the id the destination knows (XF new id via
import_log, or XF old id via Discourse’sthreads/{id}permalink). 301 everything else that means the same thread. - Room URLs. Map old forum/category paths to the new room. If names were ugly, fix names after you confirm the permalink key.
- Index, What’s New, members. 301
/to/, What’s New to the destination’s equivalent (/whats-new/or/latest). Do not leave two homepages. - Graveyard paths (resources, gallery, page nodes, cart leftovers). One hub or a clean 404. Do not 200 an empty shell.
- Query-string and
/index.phpleftovers. Fold into the same extract-id rules.
Search Console / a sitemap resubmit happens after the hostname is the hostname members use. The XF SEO and Discourse SEO articles own the rest. This runbook only needs: every 200 you still care about has a 301.
Permissions and staff on night one
You will not finish the overlay before DNS. You must finish safety:
- Guests cannot see staff rooms.
- The people who moderate tomorrow can moderate tonight.
- Registration is the policy you meant (closed, approval, or open) while the import log is still warm.
- Outbound mail is not going to 40,000 people from staging. Mute it until the hostname is production.
Then schedule the real overlay for the following week, off-peak, with Analyze permissions (XF) or a category-permission pass (Discourse) on named test accounts.
What usually looks like an import bug
| They say | You check first |
|---|---|
| “Half the users missing” | Banned/invalid filter; merge-by-email; you counted the wrong table |
| “Everyone is Admin2” | Same-username merge was off |
| “Faces empty” | Avatar path; data/ not writable; Discourse l/ tree missing |
| “Attachments zero-byte” | Attachment directory; hashed filenames |
| “Retain IDs failed” | Destination was not empty of threads/posts |
| “Import still running tomorrow” | Browser runner + remote DB. CLI, both DBs local |
| “Private room is public” | You have not rebuilt the overlay. Expected until you do |
| “Google dropped us” | No 301s. Expected until you ship them |
| “Passwords don’t work” | Planned reset (especially XF → Discourse). Not a surprise |
| “Resources / gallery gone” | Not in the stock map. Graveyard |
Which article to open next
| Pair | Open |
|---|---|
| Should we move at all? | Old forum → modern platform |
| We run XF; should we leave? | Stay or migrate |
| phpBB 3.2–3.3 → XF | phpBB cookbook |
| XF → Discourse | xenforo.rb dry-run |
| Same XF, new host | XF backup / move |
| Any product, what to copy | Backup inventory |
| Feature matrix | XF vs Discourse 2026 |
| Money | Three-year TCO |
Worked skeleton: two nights
Night −14. Inventory sheet. Snapshot. Staging destination. Dry-run import. Write the miss list (gallery, prefixes, passwords). Draft member mail: what survives, what dies, how to log in.
Night 0. Freeze source (or take the final snapshot and freeze). Import on the working box if you are not reusing the dry-run database. Destination backup. Restore to production. 301s. DNS. Staff smoke test (login, one old avatar, one old attachment, one private room, five old URLs). Board active / equivalent on when you mean it.
If the dry-run was eight hours, night 0 is not a Sunday at 22:00.
Special pairs the general map still covers
XenForo → XenForo (absorb a second board). Official source. You cannot retain IDs if the destination already has threads. Merge-by-email is the default. Plan that thread numbers will be new and that the import log is the 301 key. This is not a host move.
Discourse 3.x → XenForo. Official source into XF. Treat it as “we liked the forum tree after all,” not as a one-click undo of xenforo.rb. Page nodes, XFRM, and XF permissions will not reappear. You will rebuild them. Permalinks go through import_log like any other into-XF import.
IPS 4.x → XenForo. On the official list. Suite apps (calendar, commerce, pages) are the graveyard. The IPS vs XenForo piece is the product choice; this runbook only says: inventory those apps before you promise members “everything comes across.”
vBulletin / SMF / MyBB → XenForo. Official ranges as listed. vBulletin has extra official notes (import-notes-vbulletin) because attachments on disk are a path sport. Read that page. Do not guess.
Flarum or NodeBB as source or destination. No official XF importer. No stock xenforo.rb equivalent this article will name. Budget a community script or a manual export, and say so in the member mail.
Member mail (send this, not a feature matrix)
Three short paragraphs, in the order people actually care:
- How you log in on Monday. Same password / reset / Discourse ID / SSO. Be honest if it is a reset.
- What URL to use. Old bookmarks should redirect; if some cannot (resources, gallery), link the hub.
- What is gone. One bullet list. Credits, gallery, a page-node FAQ, a listing field. If the list is empty, say that too.
Do not lead with “we upgraded to a modern platform.” Lead with the password and the bookmark.
Staff get a longer mail: who can still moderate, where the private rooms went, and that permission tidy is scheduled off-peak because a groups × nodes rebuild can lock a large XF tree.
What this is not
It is not a stay/go essay. It is not a phpBB field-by-field, not a Ruby walkthrough, not a Discourse category lecture. It will not invent retain-ID rules beyond the official one: retain IDs only on an empty XenForo (no threads, no posts). It will not invent a Flarum official importer.
If you do not have a destination backup you have restored once, you do not have a cutover. You have a hope.
Takeaways
- Import is not a host move. Map users, posts, attachments, permissions, permalinks. Rebuild or eulogize the rest.
- Official XF importers go into XenForo and include Discourse 3.x as a source. XF → Discourse is xenforo.rb.
- Retain IDs only on empty XF. Redirects use the import log, not matching numbers.
- Source files matter. Empty faces are a path.
- Permissions and URLs are week-one projects. Safety and 301s are night-zero projects.
Write the five-object sheet. Open the specific cookbook for your pair. Take the destination backup. Then start the importer — not before.

