CoDNA Request access
Contents

Docs How it works

Export

Status: built. codna export, codna import, and codna ingest all exist.

Export is not a backup format. It is the answer to "what happens if we stop using CoDNA?" — and the answer has to be a directory of markdown files that are useful on their own.

The tree

A document at address wiki:engineering/onboarding/laptop-setup exports to wiki/engineering/onboarding/laptop-setup.md:

codna-export-2026-09-24/
├── wiki/
│   ├── engineering/
│   │   ├── onboarding.md
│   │   ├── onboarding.files/
│   │   │   └── network-diagram.png   ← the page's images and PDFs, beside it
│   │   └── onboarding/
│   │       ├── laptop-setup.md
│   │       └── vpn-access.md
│   └── ops/
│       └── deploy.md
├── raw/
│   └── 2026/09/
│       ├── it-handbook.md
│       └── it-handbook.pdf        ← the original, alongside its conversion
├── output/
│   └── 2026-q3-summary.md
└── codna-manifest.json

Because links are stored as relative .md paths (linking.md), the tree works as-is: open it in Obsidian, push it to a git repository, browse it on GitHub. Every link resolves. That is the entire reason the link format was chosen over [[wikilinks]] or codna:// URIs.

Originals from raw are exported next to their converted markdown, so the evidence survives too.

A page's images and PDFs (files.md) are written into a folder beside it named after its slug, onboarding.files/, which is where the page's own links to them point: ![Network diagram](onboarding.files/network-diagram.png) shows the picture in Obsidian or on GitHub as it does in CoDNA. Every file the page holds is written, whether or not its current version shows it, since an older version may.

Frontmatter

Columns are canonical; frontmatter is how they serialize. Export writes them back out:

---
id: 018f3c2a-7d41-7c9e-b3a5-2f1e8c4d6a90
title: Laptop Setup
space: wiki
path: engineering/onboarding
slug: laptop-setup
tags: [onboarding, it]
status: active
version: 7
created: 2026-09-22T10:14:00Z
updated: 2026-09-24T08:02:00Z
author: tornike@example.com
---

See the [onboarding guide](../onboarding.md) before starting.

Reserved keys

CoDNA owns these. On import they populate columns; on export they are rewritten from columns:

id · title · space · path · slug · tags · status · version · created · updated · author · source

Every other key is preserved verbatim in frontmatter_extra and written back unchanged. Someone else's reviewed_by: or confluence_id: survives a round trip through CoDNA untouched — a knowledge base that quietly drops metadata it does not recognize is not one people will trust with their existing files.

id is what makes re-import an update rather than a duplicate.

What is not exported

Excluded Why
acl_read / grants Access control is a property of the deployment, not of the file. A file tree that carried ACLs would imply the tree enforces them, and it cannot — anyone with the directory has everything in it.
Other people's documents An export as a person contains exactly what that person can read: their slice, never the whole. Only codna export --all, on the host, writes every document.
Tombstoned documents Unless --include-deleted
Revision history Unless --with-history; see below

The first row is the one to be deliberate about. It is tempting to write permissions into frontmatter for round-tripping, and it would mislead: the export is a flat directory, and a directory has no opinion about who reads it.

History

Full snapshots per revision (data-model.md) make history export straightforward:

--with-history →  _codna/history/<document-id>/
                    0001-create.md
                    0002-content.md
                    0005-move.md

Each file is a complete document with its frontmatter, plus change_kind, change_summary, and the author. Diffs are not exported — they are derivable from the snapshots, and git diff on the exported tree is better than anything CoDNA would pre-render.

Manifest

{
  "codna_version": "0.1.0",
  "exported_at": "2026-09-24T09:00:00Z",
  "exported_by": "tornike@example.com",
  "scope": { "spaces": ["wiki", "raw"], "complete": false,
             "include_deleted": false, "with_history": false },
  "counts": { "documents": 412, "revisions": 0, "originals": 87, "files": 140 },
  "documents": [
    { "id": "018f3c2a-…", "address": "wiki:engineering/onboarding",
      "file": "wiki/engineering/onboarding.md", "version": 7,
      "files": ["wiki/engineering/onboarding.files/network-diagram.png"] }
  ]
}

The manifest states the scope explicitly. An export that silently omitted documents the actor could not read, without saying so, would be mistaken for a complete backup — and the moment that matters is the moment nobody can check. scope.complete is true only for codna export --all of every space, whose exported_by reads management, with codna export --all; a person's export names the person and is never complete.

Import

Import is export in reverse, and the same code path serves migrating in from Obsidian or a docs repository:

  1. Walk the tree; the first path segment names the space.
  2. Parse frontmatter. Reserved keys populate columns; the rest goes to frontmatter_extra.
  3. id present and known → update as a new revision. id absent or unknown → create.
  4. Extract links after every document exists, so cross-references resolve rather than landing as pending.
  5. Missing frontmatter is fine: title comes from the first # heading or the filename, path and slug from the file's location.
  6. A page's <slug>.files/ folder is read back into it. A file the page does not hold is added under its name; one it holds with the same bytes is left alone, so re-importing an export adds nothing; one it holds with other bytes is reported and skipped, because files do not change. A name has to be one CoDNA would give the file (lowercase words joined by dashes, with the extension of what the bytes are), or the page's link to it would not reach it, and is reported otherwise.

Step 4 matters for large imports. Resolving links as each file lands would leave every forward reference pending until something else happened to touch it.

Commands

Command Does
codna export --as <user> Everything that person can read, as a directory
codna export --as <user> --space wiki One space
codna export --as <user> --with-history --include-deleted All of that person's slice, with history and tombstones
codna export --all Every document, whoever can read it: a backup or a move. Host only
codna export --all --with-history --include-deleted Every document and every revision, for migration or archival
codna import --as <user> <dir> Read a tree back in, matching on id then address
codna import --as <user> --dry-run <dir> Report what would be created and updated
codna import --as <user> --space wiki <dir> Import a vault whose paths do not name a space
codna ingest --as <user> <dir> Bulk-load unstructured files into raw
GET /api/me/export?with_history=&include_deleted=&with_originals=&spaces= What the signed-in person can read, as a streamed .tar.gz of the same tree

The HTTP form is a person's: it lets someone take what they can read without a shell on the host. It runs as the caller, so its manifest is never complete, and a management token is refused it like every other content route. No API call exports everything (adr/0013). Import over HTTP is deliberately absent; codna import covers restore, and a write of that size belongs on a shell.

import and ingest are different operations and it is worth being clear which you want. import reads a structured tree — the first path segment names a space, frontmatter carries ids — and preserves addresses, so it round-trips an export and migrates an Obsidian vault. ingest takes a pile of PDFs, Word files and HTML with no structure to preserve, converts each one, and files the results in raw by date.

Raw on import

raw is immutable, which makes it the one interesting case.

  • A raw document that is already present is left exactly as it is. Re-importing an export must not rewrite the record of what was received, so it is reported as unchanged rather than as an error.
  • A raw document that is absent is restored. This is the case that matters when rebuilding a deployment from nothing but the directory: without it every wiki page cites evidence that no longer exists.

Restoring raw is one of exactly two code paths allowed to write there; the other is ingestion. No ingestions row is created, because the original bytes are not in the tree and recording an ingestion that never happened would be worse than recording none.

One of --as and --all is required on export, and which one is recorded in the manifest. --all is the one read in CoDNA that no permission bounds. It exists only as a command on the host, because whoever can run it there already holds the database; no API call, and no token, can make it. Each run is recorded in the management audit as export.all, with how much it wrote. An --as export prints a line saying it is one person's slice, because an export that quietly contained one person's slice would be mistaken for a backup at exactly the wrong moment.

Originals from raw are written beside their conversions, except when the original is markdown — pasted text, or an uploaded .md. Copying those would write two near-identical files, and import would then read the copy back as a second document.