# Migrate Notion to Confluence

> Confluence is a strict tree — one space, one parent per page — while Notion is a graph, and most of the work is in reconciling the two.

Source: https://migratly.dev/migrate/notion-to-confluence/

Migratly migrates Notion to Confluence by reading both APIs live, deriving the mapping between them, and
presenting it for approval before anything is written. This pair covers **8 entity types**
and **53 field mappings**: 24 carry across intact, 28 are flagged as lossy,
partial, dropped or needing another route, and 1 needs a decision from you. The Notion side is
read-only for the whole run.

## How the structure changes

- A Notion workspace or top-level page becomes a Confluence space.
- A Notion database becomes a parent page whose children are its rows — Confluence has no database or row concept.
- Notion blocks are converted to Confluence storage-format XHTML by the connector spec, not field by field.
- Inline page mentions, link_to_page blocks and child pages are queued and resolved in a second pass that handles cycles, forward references and orphaned sub-pages.
- Pages with no content still arrive, carrying a spec-declared panel that links back to the Notion original.

## Entity map

| Notion | Confluence | Wave | Fields |
|---|---|---|---|
| Database | Space | 0 | 5 |
| User | User | 0 | 4 |
| Block | InlineXHTML | 1 | 3 |
| ChildPageBlock | Page | 1 | 2 |
| Page | Page | 1 | 28 |
| Attachment | Attachment | 2 | 4 |
| Comment | FooterComment | 2 | 5 |
| Relation | InlinePageLink | 3 | 2 |

## Field coverage

### Database → Space

| Notion field | Confluence field | Coverage | Notes |
|---|---|---|---|
| `title` | `name` | Full | Notion database titles are rich_text arrays — flatten to plain text for the space name. |
| `id` | `key` | Workaround | Confluence space keys are uppercase alphanumeric. Generated from the Notion database id (first 8 hex chars, prefixed with NTN). |
| `description` | `description` | Partial | Notion description is rich_text — converted to a single plain paragraph for the space description. |
| `icon` | no equivalent | Dropped | Confluence space icons are uploaded separately via /space/{key}/setting and are out of scope for v1. |
| `properties` | no equivalent | Workaround | Notion database property schema is rendered as a Page Properties summary on each child page (per row). The schema itself is preserved as a page property on the database-parent page. |

### User → User

| Notion field | Confluence field | Coverage | Notes |
|---|---|---|---|
| `id` | `Original Notion User ID` | Lossy | Confluence users are identified by accountId. Notion user IDs are preserved for audit only. |
| `name` | `displayName` | Full |  |
| `person.email` | `email` | Full | Email is the join key. Build the Notion userId -> Confluence accountId table before any pages migrate. |
| `type` | no equivalent | Dropped | Notion bot users have no Confluence equivalent and are filtered out. |

### Block → InlineXHTML

| Notion field | Confluence field | Coverage | Notes |
|---|---|---|---|
| `type` | `_xhtml_tag` | Partial | Most Notion block types map to a clean storage XHTML element. Edge cases: synced_block flattens, button drops, equation requires Math macro app. |
| `{block_type}.rich_text` | `_xhtml_inline` | Partial | Annotations bold/italic/strike/code/link map directly. Color annotations preserved via inline style. User mentions require user_mapping_table. |
| `{block_type}.children` | `_xhtml_nested` | Full | Nested blocks recurse into nested XHTML containers (e.g. nested  inside ). |

### ChildPageBlock → Page

| Notion field | Confluence field | Coverage | Notes |
|---|---|---|---|
| `child_page.title` | `title` | Full |  |
| `_parent_block.page_id` | `parentId` | Full |  |

### Page → Page

| Notion field | Confluence field | Coverage | Notes |
|---|---|---|---|
| `id` | `_page_property:Notion Page ID` | Full |  |
| `properties.title` | `title` | Full |  |
| `parent.database_id` | `parentId` | Full |  |
| `parent.page_id` | `parentId` | Full |  |
| `_blocks` | `body.value` | Partial | Notion's block model is richer than Confluence storage XHTML. Most blocks convert directly; formula/rollup compute to text; synced_blocks flatten; buttons drop. |
| `_comments` | no equivalent | Workaround | Notion comments are per-block. Confluence's API only supports page-level (footer) comments. Each Notion comment becomes one footer comment with a leading author/date line. |
| `properties.{select}` | `_page_property + label` | Full |  |
| `properties.{multi_select}` | `_labels` | Full |  |
| `properties.{status}` | `_page_property:Status + label` | Full |  |
| `properties.{date}` | `_page_property` | Full |  |
| `properties.{number}` | `_page_property` | Full |  |
| `properties.{checkbox}` | `_page_property` | Full |  |
| `properties.{url}` | `_page_property + body_link` | Full |  |
| `properties.{email}` | `_page_property` | Full |  |
| `properties.{phone_number}` | `_page_property` | Full |  |
| `properties.{people}` | `_page_property + body_mention` | Partial | Confluence mentions require accountId. Notion users without a matching Confluence email fall back to plain text 'Originally assigned to [name]'. |
| `properties.{files}` | `_attachments` | Partial | Notion-hosted files have signed URLs that expire in 1 hour — must be downloaded and re-uploaded to Confluence during the same run. External URLs are kept as link page properties. |
| `properties.{relation}` | `body.value` | Partial | Confluence has no native relation type. Each related Notion page id is rendered as an inline  link inside the page body in a 'Related' section. Requires all pages created first. |
| `properties.{rollup}` | `_page_property` | Workaround | Notion rollup logic is not reproducible in Confluence. The computed value at migration time is captured as a frozen page property. |
| `properties.{formula}` | `_page_property` | Workaround | Notion formula logic is not reproducible in Confluence. The computed value at migration time is captured as a frozen page property. |
| `created_time` | `_page_property:Original Created` | Workaround | Confluence createdAt is server-set. Original timestamp preserved as page property. |
| `last_edited_time` | `_page_property:Original Updated` | Workaround | Confluence updatedAt is server-set on every PUT. Stored as page property for audit. |
| `created_by.id` | `_page_property:Original Author` | Workaround | Confluence cannot set page author. Original Notion author preserved as page property; if a Confluence mapping exists, also rendered as an info-panel at the top of the body. |
| `last_edited_by.id` | no equivalent | Dropped | Already covered by created_by; tracking the last editor per-page in Confluence is automatic going forward. |
| `icon` | no equivalent | Dropped | Confluence pages have no native emoji/icon field via API. Could be rendered as a heading prefix — opt-in only. |
| `cover` | no equivalent | Dropped | Confluence pages have no cover image field. |
| `archived` | `status` | Full |  |
| `url` | `_page_property:Original Notion URL` | Full |  |

### Attachment → Attachment

| Notion field | Confluence field | Coverage | Notes |
|---|---|---|---|
| `file.url` | no equivalent | Partial | Notion-hosted: download bytes, then multipart upload to Confluence. External-URL files: skip rehost, render as inline link in the page body. |
| `name` | `filename` | Full |  |
| `_size` | no equivalent | Internal | Used to skip files >100MB (Confluence default per-file limit). |
| `caption` | `_xhtml_inline` | Full | Notion file caption rendered as the inline link/figure caption in the page body next to the attachment reference. |

### Comment → FooterComment

| Notion field | Confluence field | Coverage | Notes |
|---|---|---|---|
| `rich_text` | `body.value` | Partial | Notion comments are anchored to a specific block. Confluence footer comments are anchored to the whole page — the block context is lost (unless inline-comment anchors are added in a second pass, which is out of scope for v1). |
| `created_by.id` | no equivalent | Workaround | Confluence cannot author comments as another user. Original commenter name is prepended to the body. |
| `created_time` | no equivalent | Workaround | Confluence comment createdAt is server-set. Original timestamp inlined into the prepended header. |
| `discussion_id` | no equivalent | Dropped | Notion comment threads (discussion_id) have no Confluence equivalent on footer comments. Replies flatten to siblings under the same page. |
| `parent.block_id` | `pageId` | Partial | Notion comments anchor to a block; we re-target the comment to the block's page. The exact in-page location is not preserved. |

### Relation → InlinePageLink

| Notion field | Confluence field | Coverage | Notes |
|---|---|---|---|
| `relation[].id` | `body.value._related_section` | Partial | Confluence has no relation type. Each related Notion page id resolves to a Confluence page; inline links are appended under a 'Related' heading. No back-relation is created. |
| `_property_name` | `body.value._related_section.heading` | Full | Notion lets multiple relation properties exist per page (e.g. 'Blocked by', 'Related to'). Each becomes its own subheading in the Related section so the original semantics are preserved. |

## FAQ

**Can a Notion database be migrated to Confluence?**

Yes. Confluence has no database object, so the database becomes a parent page and each row becomes a child page beneath it. Database properties become page properties or labels depending on their type.

**Do Notion page links still work after migrating to Confluence?**

Yes. Inline mentions and page links are rewritten to Confluence ri:page references in a resolution pass that runs after every page exists, which is what makes circular and forward references resolvable.

**What Notion content does not convert to Confluence?**

Blocks with no Confluence equivalent — synced blocks, embedded databases and some third-party embeds — are the usual gaps. Every one is listed in the coverage report before the run, so nothing is discovered after the fact.

---

Start a migration: https://app.migratly.dev/
