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 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.

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 safe to treat as a real
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. 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 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">
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>