The OKF bridge
Subspace can mirror its entire graph to a folder of Markdown files, one file per page, and
keep that folder and the database in continuous, two-way agreement. This is the OKF bridge
(@subspace/okf). It is what lets agents edit pages by submitting Markdown, what makes
GitOps deploy configuration, and what lets you keep your knowledge in
plain files under version control while the app stays authoritative.
The codec
Section titled “The codec”At the center is a pure codec: a line-based parser and printer that maps a page document to and from Markdown. The mapping is deterministic:
| Document construct | On disk |
|---|---|
| A page | <dir>/<slug>.md |
| id, title, aliases, metadata | YAML frontmatter |
| Bullets | Markdown list items |
| Nesting | indentation |
| An element (table, code cell, terminal, function, file) | a fenced block tagged with the element type plus a JSON payload |
| Stable node ids | an HTML-comment suffix on each bullet |
| A directory page | index.md |
---id: 9c2e1a…title: Q3 planningaliases: [Q3, Third quarter]metadata: { srs: { enabled: true } }---
- Revenue targets <!-- node:7f3a --> - cash: $610,000 <!-- node:8b1c -->- Open questions <!-- node:22de -->
```subspace:table {"name":"pipeline"}{ "cells": { "A1": {"v":"Lead"}, "B1": {"v":"Value"} } }The codec's defining property is round-trip stability: **`parse(print(doc))` equals `doc`**,enforced by property-based tests. That is what makes the disk mirror safe to treat as a realrepresentation of the graph rather than a lossy export.
<Aside type="note"> Stable node ids ride along as HTML-comment suffixes (`<!-- node:8b1c -->`). They are what make a three-way merge possible per node even after an external editor reflows the file. If the comments are stripped, the merge falls back to position-and-text matching.</Aside>
## Three uses of one codec
The same codec is used three ways, so there is one implementation to trust:
<CardGrid> <Card title="Agent edits"> An agent's `kb.edit` submits OKF Markdown and the codec diffs it to node commands **in memory**, with no filesystem round trip. See [tools and approvals](/agents/tools-and-approvals). </Card> <Card title="The disk dump"> The export consumer continuously writes changed pages to the `okf/` folder, the surface that coding agents and [GitOps](/deployment/gitops) work against. </Card> <Card title="Import"> A file watcher folds external edits back into the graph as commands. </Card></CardGrid>
## The export consumer
Export is an [outbox consumer](/architecture/outbox-dispatcher): when a page changes, itrewrites that page's file. It is backed by an `okf_manifest` table keyed by **page id**(`page_id`, `path`, `content_hash`, `last_export`), and keying by id is what makes renamesfirst-class: a renamed page keeps its manifest row and simply changes its `path`.
Before every atomic write, the exporter is **anti-clobber**:
<Steps>
1. **Re-hash the disk file**
It re-hashes the file on disk against the manifest's `content_hash`.
1. **Import first on a mismatch**
A mismatch means an external edit landed after the last export, so the exporter imports that edit first (the three-way merge below), then re-exports the merged document. The import command and the manifest update commit in one transaction, closing the export-over-external-edit clobber window.
</Steps>
The exporter also owns the **file lifecycle** so the two directions never fight:
- A page create writes a new file.- A page delete **moves the file to `okf/.trash/`**, never `unlink`.- An in-app rename or move writes the new file and trashes the old one, with both manifest rows committed *before* touching disk, so the watcher sees a create-and-delete it already expects and stays silent.
<Aside type="caution"> A residual sub-millisecond race remains: an external write that lands between the re-hash and the rename. It is accepted and stated plainly. Postgres stays authoritative, and the worst case is losing one external save.</Aside>
## The import watcher
Import uses `chokidar` to watch the `okf/` folder. When a file's hash no longer matches themanifest, an external edit happened, and the codec parses it into a **three-way merge pernode**:
- **base** is `last_export` (the merge base recorded in the manifest),- **theirs** is the disk version,- **ours** is the current document.
Nodes changed only on disk apply. A node changed on both sides resolves last-write-wins withdisk winning, the one accepted caveat, narrowed to node granularity. **Concurrent in-appedits to other nodes always survive.** Each file lands as one `applyOkf` command in onetransaction.
Inbound file operations are classified against the manifest, and import is **neverdestructive**:
| On disk | Interpreted as ||---|---|| A new file | an implicit `createPage` (the id is minted server-side and written to frontmatter on the next export pass) || A deleted file | *not* a page delete: the page moves to kb-side trash and a "confirm or restore" task card opens || A rename or move (matching frontmatter id or content hash) | a real `renamePage` / `movePage`, so `[[refs]]` rewrite and backlinks survive |
Only an id-less move that also changes content degrades to a create plus a confirm-deletecard. Scheduled external agents (a cron'd coding-agent run over the dump, say) take the OKFsingle-flight advisory lease so their edits and the exporter never interleave.
<Aside type="note"> A deleted file never destroys a page. The strongest thing an errant `rm` in the folder can do is open a task card asking you to confirm or restore.</Aside>
## Related
<CardGrid> <LinkCard title="GitOps" href="/deployment/gitops" description="Deploying configuration and pages through the OKF folder in git." /> <LinkCard title="Coding agents" href="/automation/coding-agents" description="Running coding agents over the disk mirror." /> <LinkCard title="The command model" href="/architecture/command-model" description="The applyOkf command and atomic rename the bridge relies on." /> <LinkCard title="Pages & directories" href="/knowledge-base/pages-and-directories" description="Slugs, directories, and index pages as they map to files." /></CardGrid>