Skip to content

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 lets agents edit pages by submitting Markdown, lets GitOps deploy configuration, and keeps your knowledge in plain files under version control while the app stays authoritative.

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 planning
aliases: [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 a faithful
representation 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, it
rewrites 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 renames
first-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. 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 the
manifest, an external edit happened, and the codec parses it into a **three-way merge per
node**:
- **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 with
disk winning, the one accepted caveat, narrowed to node granularity. **Concurrent in-app
edits to other nodes always survive.** Each file lands as one `applyOkf` command in one
transaction.
Inbound file operations are classified against the manifest, and import is **never
destructive**:
| 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-delete
card. Scheduled external agents (a cron'd coding-agent run over the dump, say) take the OKF
single-flight advisory lease so their edits and the exporter never interleave.
<Aside type="note">
The strongest thing an errant `rm` in the folder can do is open a task card asking you to
confirm or restore the page.
</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>