Basalt docs
GitHub

Import — and what it costs

Basalt imports from Notion, Confluence, Obsidian and plain Markdown folders. This page is about the other half: what does not survive the trip.

It exists because the honest answer to "can I move my wiki over?" is never just "yes". Every source holds things Basalt has no place for, and every export format has already thrown some of them away before Basalt sees the file. Knowing which ones, before you migrate, is the difference between a decision and a surprise.

Two rules the importer follows everywhere:

  • Nothing is dropped silently. Anything lost is counted in the run report, with examples. The report is kept — Settings → Import shows past runs.
  • A re-run converges instead of duplicating. Page ids are derived from the source path, so importing the same space twice updates the same pages. The consequence is on this page too, under Renames.

What is lost from every source

What Why Report code
Timestamps Created/updated dates become the import's own. The source's dates are in the export, but writing them would make "last edited" lie about who to ask. timestamps-not-preserved
Authors "Created by" names a person in another system's user directory. Basalt does not invent accounts to match. authors-not-preserved
Comments, reminders, page history Not imported — see Why comments stay behind below. Page history starts fresh here. comments-not-imported
Database views Filters, sorts, grouping and layout. A Notion export ships one CSV per view with the view already applied; the API has no endpoint for view definitions at all. The data arrives, the way you were looking at it does not. views-not-imported
Person columns Arrive as display names. A name is not a workspace account, and guessing which colleague "M. Schmidt" is would silently assign work to the wrong person. people-as-text
Relation columns Arrive as target titles, kept as text. A title is not an edge; two pages can share one. relations-as-text
Formula and rollup columns Exports contain the computed result, never the expression. The number is right today and frozen forever. formulas-as-text

Where an import lands

An import writes into one collection, and inside it the source's tree is reproduced as it stands:

  • One Confluence space → one collection. Its top-level pages become the collection's top-level pages. Nothing is inserted in between.
  • Several spaces in one connection get a page each, named after the space, because a collection cannot contain another collection. If you would rather have one collection per space, make one connection per space and point each at its own — the two are otherwise identical, and a link from one space into another that is not in the same run stays text and is counted (links-unresolved).

Note that this decides the identity of every imported page: ids are derived from the target collection and the source path, so moving a connection to a different collection — or splitting one connection into several — makes the next run create pages rather than update them. Decide this before the first real import, not after.

Why comments stay behind

Both systems do expose them: Confluence has footer and inline comments on its API, Notion has page and block comments. They are not imported, and the reason is not the reading:

  • A comment is written by somebody. It carries an author from the other system's directory, and Basalt does not invent accounts to match (the same rule that makes "created by" the person who ran the import). A discussion imported with every line attributed to the importer is worse than no discussion — it puts words in people's mouths.
  • An inline comment is anchored to a range of text. That anchor is stated against the source's own markup, which conversion to Markdown has changed. A reply that lands on the wrong sentence is a misquote.
  • Resolved threads, reactions and mentions inside comments compound both problems.

What that means for a migration: the discussion stays in the system you are leaving. Keep the wiki readable until the conversations in it no longer matter, or copy the handful of threads that do into the page body by hand.

Renames and moves

A page's identity comes from its path in the source, and the path contains the title. Rename or move a page in the source, and the next run creates a new page in Basalt rather than updating the old one. The old page stays, with its content, until you delete it.

This is the price of not needing the source to cooperate: no plugin, no write access, no id Basalt could have stored on the other side. For a one-time migration it does not matter. For a connection you re-run weekly, it does.


Confluence

Connected with an API token (Settings → Import), read-only — Basalt never writes to your wiki.

What State Report code
Pages, hierarchy, tables, lists, code Imported. Expand macros become toggles, status macros become text. A code macro keeps the language it declared, so the block is highlighted here too. A code block inside a list item stays inside it.
The order of the pages Imported. Confluence records where a page sits among its siblings only for a branch somebody has actually arranged; an unarranged branch is shown alphabetically there, and arrives alphabetically here.
Page titles Imported exactly as the wiki spells them, punctuation included. The file a page is fetched into has /, :, * and " replaced by -, because a path cannot hold them — the page's name does not.
Task lists Imported as task lists, ticked or not, as they stood in Confluence.
@mentions Lost. Confluence stores a mention as an account number and looks up the name when it draws the page, so the name is not in the wiki's own storage — and the account belongs to that system's directory. A sentence that read "ask @Anna" now reads "ask", which is why it is counted. confluence-mentions-not-imported
Info / note / tip / warning panels Imported as callouts, with the icon that matches the kind of panel it was. The panel's own title becomes the callout's first line.
Page icons Imported, when the wiki will say what they are. The emoji lives in an undocumented Confluence property that only the older API reads in bulk, so on some instances there is simply no answer — the page then arrives without an icon and everything else is unaffected. An icon chosen in Basalt is never overwritten by a re-run that finds none.
Bullet lists pasted out of Word Imported as real lists. Confluence stores such a paste as ordinary paragraphs beginning · followed by hard spaces; Basalt turns a run of them back into list items. A bullet inside a table cell is left as text — a table cell cannot hold a list.
Empty list items Dropped. <li></li> is residue from an item somebody cleared; an item that holds only a nested list is kept, because that is a real level of the outline.
Block content inside a table cell Flattened onto one line: a list becomes • one • two, a code block becomes inline code, two paragraphs are joined by a space. A cell holds rich text here (and in Markdown), so a list in one could never have survived — but a line break inside a cell would have ended the row and taken the rest of the table with it.
Column layouts (two- and three-column sections) Flattened to their contents, in reading order: left column first, then the right. Everything inside them — including whole tables — arrives intact.
Images on a page (PNG, JPEG, GIF, WebP) Imported — the file comes across and the page shows it.
Documents and media (PDF, Word, Excel, PowerPoint, OpenDocument, text, MP4/MOV/WebM, MP3/WAV/Ogg) Imported. The file comes across and the page keeps a link to it, named as it was named in the wiki.
Archives, programs, scripts, SVG (.zip, .exe, .js, .svg) Not imported, on purpose — see What Basalt will not store below. The reference stays as text. confluence-attachments-skipped
Attachments nobody embedded Not imported. A file in a page's attachment tab that no image or macro shows would arrive as something nothing points at.
A picture that hangs on another page Imported, from the page that owns it — both pages then show the one file. If that page is not part of the import, the reference is reported instead of being satisfied from the showing page's own files. confluence-attachments-skipped
Links between spaces Imported with their space. A link into a space this run did not import stays as text and is counted. links-unresolved
Links pasted as a full URL Imported. A https://…/wiki/spaces/KEY/pages/123/Title pointing at a page this run brought over becomes an internal link; anything else stays as it is, and is counted when it addresses this wiki. source-urls-not-rewritten
Macros with no Basalt equivalent The text inside survives; the macro does not. confluence-macros-unsupported
Page restrictions Not read. Everything lands with the collection's permissions — check them after a first import.

Confluence addresses a page by title, and titles are unique inside a space — nothing stops two spaces from each having an "Onboarding". A link that leaves its space says so (ri:space-key), and Basalt carries that through: a cross-space link is resolved in the space it named, or not at all.

What follows from that is a choice you make once, when you set the connection up — and it is the same choice as Where an import lands above:

  • All the spaces in one connection. Cross-space links land on the page in the right space, even where several spaces use the same page title. Each space gets a page of its own inside the collection.
  • One connection per space, one collection per space. The cleanest tree — the collection is the space — and the price is that a link from one space into another stays plain text and is counted under links-unresolved. The two spaces are separate collections, and an internal link between them is not something the importer can invent.

Adding a second space to a collection that already holds one is the case to avoid: the two spaces' top-level pages end up side by side, and a page title that exists in both stops resolving for either (see below).

A link is never resolved by guessing. Where two pages could be meant and nothing says which — the same title in two spaces, and a link that named none — Basalt leaves the text alone and counts it. A missing link is visible in the report and repairable; a link that quietly points at the wrong page is neither.

Not every internal reference in a wiki was made with the link picker. Most were made by copying the address out of the browser, and those are the ones that keep the old system alive after a migration: they still work, so nobody notices them.

Basalt rewrites them too. A pasted address is matched on the numeric page id Confluence puts in the URL — so a link copied before the page was renamed still finds it — or, failing that, on the exact address. Notion works the same way, using the 32-hex id in its URLs (this one also works for a Notion export archive, where nothing recorded a live URL).

Four kinds are deliberately left pointing at the old system, because in each of them a rewrite could land on the wrong thing — and a wrong link is invisible where a missing one is not:

  • Another instance. Two wikis use the same URL shape and the same page numbers; only the host tells them apart. A URL that merely contains your wiki's host (as a query parameter, or as a prefix of a longer domain) is another site.
  • A page this run did not import. Counted under source-urls-not-rewritten — import the space it names into the same collection and run again, and the link repairs itself.
  • A link into part of a page#a-heading, a focused comment, an attachment preview. An internal link has no way to carry the part, and delivering you to the top of a long page instead is the wrong place.
  • A link to another website, whose last path segment happens to match one of your page titles.

Attachments, in detail

Basalt fetches the attachments a page actually shows — an <ac:image>, a view-file/viewpdf macro, or a link to a file — and stores the ones it has a home for. A page that shows a file belonging to another page gets it from that page, so the file is fetched and stored once and both pages show it. An image arrives as a picture on the page; anything else arrives as a named link to the file. A skipped file leaves the page's text untouched and its reference as plain text, and is counted in the report.

Four bounds apply, in this order:

  1. What the file is — see the next section. The decision is made from the file's content, not from its name.
  2. Per file — 25 MB, or the plan's file-size limit if that is lower. A single attachment above it is reported, never truncated.
  3. The workspace's storage plan. Attachments are fetched while there is room and stop at the boundary. Nothing is ever written past the limit.
  4. 400 MB per run, even on a plan with unlimited storage — one run should not pull a media library into memory.

There is no per-type ceiling: a PDF and a PNG are held to the same 25 MB.

The same two plan limits apply to an uploaded archive (a Notion zip, a folder of Markdown), which used to bypass them: a file above the plan's per-file cap is reported as skipped, and once the workspace is full the remaining files are left with storage-limit-reached. The pages still land — only their attachments are missing — and importing the same source again after making room adds exactly those.

When the storage runs out mid-import: the import does not fail. Every page still lands, with its text, its structure and every attachment that fitted; the rest are listed under confluence-attachments-quota. Make room or move to a larger plan and run the connection again — page and attachment ids are derived, so the second run adds exactly what was missing rather than importing everything a second time.

That last property has a useful consequence: an attachment is keyed by its Confluence page id, not by the page's title path. So unlike a page (see Renames above), renaming a page in Confluence does not re-upload its attachments.

What Basalt will not store

Stored, whichever way they arrive — an upload, a wiki, an archive:

Kind Types
Images PNG, JPEG, GIF, WebP
Documents PDF; Word, Excel, PowerPoint (.docx/.xlsx/.pptx and the older .doc/.xls/.ppt); OpenDocument (.odt/.ods/.odp)
Text .txt, .log, .csv, .tsv, .md
Media MP4, QuickTime (.mov), WebM; MP3, WAV, Ogg

Only the images are shown inline. Everything else downloads when you open it — including PDF, which is an active document format, so Basalt does not render it in the page where your session lives.

Basalt serves every attachment from its own address, and a public share serves them to anyone holding the link. That makes "which files may we keep" a question about what a browser might do with them, so three kinds are refused — and the refusal is decided by the file's content, never by its name (see ADR-0021):

Refused Why
SVG, HTML, XML They are documents that can carry scripts. Served from the address your workspace lives at, one of them can act as the person who opens it. Export the picture as PNG and attach that.
Programs, installers, scripts (.exe, .msi, .jar, .sh, .js, …) A store that takes any file and hands it to anyone with a link is a malware host, on your invoice and under your domain name.
Archives (.zip, .tar.gz, .7z, .rar) Same reason, one step removed. (Office and OpenDocument files are ZIP containers internally and are stored — they have to announce themselves as documents to get in.)

Two consequences worth knowing before you migrate:

  • A file whose name lies is refused. An HTML page called invoice.pdf does not come in, and neither does an SVG called diagram.png. This is the same check that makes the type shown in Basalt trustworthy.
  • Nothing you can store is ever rendered as a page. Only images are shown inline; everything else downloads.

Notion

Two ways in, and they are not equivalent.

By API key (Settings → Import) — read-only, re-runnable, and it reads Notion's typed block tree, so it knows what a callout is rather than what it was rendered as.

By export archive (Markdown & CSV) — one-off, no credentials, and it can only recover what Notion chose to write into the files.

What API Archive
Pages, nesting, formatting
Databases and rows
Images ✔ copied in ✔ copied in
Other files (PDF, Office, video) ✔ copied in ✔ copied in
Archives, programs, SVG Link kept, bytes not copied — and a Notion file URL expires within the hour, so that link will break. notion-files-not-transferred Not copied in
Toggles Become real toggles Notion flattens them to bullets before Basalt sees them
Underline, letter colour Lost — Basalt has no such marks. notion-formatting-lost Lost
Background colours Become highlight Become highlight
Column layouts, synced blocks, templates Flattened to their contents Flattened
Unknown/new block types Text kept, type named. notion-blocks-unsupported

Why underline and colour are not coming back: Markdown, which is Basalt's storage contract (ADR-0012), has no form for either. Highlight survives because <mark> is a real, semantic HTML element that other Markdown tools render. A letter colour would be an attribute no file format carries and no other tool would show — and half the palettes people pick in Notion are unreadable in dark mode.


Databases and typed columns

CSV carries no types, so the importer infers them — and refuses to guess where guessing would corrupt data.

Situation What happens Report code
01/02/2026 Kept as text. Day/month order is genuinely ambiguous and the wrong reading is a silently wrong date. ambiguous-dates-as-text
007, long digit strings Kept as text. An order number is not a number. numbers-as-text
Column name exists with another type Kept as text; the existing field is never retyped. field-key-collision
You lack "manage fields" Unknown columns are not created; their values are kept on the page. fields-not-created
A CSV row matches no single page Values imported, page body not attached. row-body-unmatched

Links that resolve to nothing are left as literal text and listed individually in the report (links-unresolved) — a link out of the imported subtree is normal, not a failure.

icon, title, aliases and databases are reserved frontmatter keys: they say something about the page itself, so they never become columns. A Markdown file or Obsidian note that carries icon: 📘 therefore arrives with that icon rather than with a field called "icon".


Before you migrate

  1. Import one space first, not the whole wiki. The report tells you which of the losses above actually apply to your content.
  2. Check permissions afterwards. Source restrictions are not read; imported pages inherit the collection's.
  3. Keep the source until you have looked at the result. Basalt does not delete anything on the other side, and neither should you.