Iron Log MD
iron-log-md is the twin of Iron Log: the same four entities, generated through the same pipeline, with one line of difference that matters — the store backend. Workouts live as editable markdown files under data/vault/ instead of SQLite rows.
It exists to make ADR 0001’s load-bearing claim checkable rather than assertable.
The source lives at examples/iron-log-md/.
Try it
Section titled “Try it”cargo run# in another shellcurl -s localhost:3001/api/workouts | jq
curl -s -X POST localhost:3001/api/workouts \ -H 'content-type: application/json' \ -d '{"id":"w-1","date":"2026-06-06","tags":["strength"],"created_at":"2026-06-06T08:00:00Z"}'
cat data/vault/workouts/w-1.md # wikilinked tags, plain YAML
$EDITOR data/vault/workouts/w-1.md # add a field, edit the prose...
curl -s -X PUT localhost:3001/api/workouts/w-1 \ -H 'content-type: application/json' \ -d '{"duration_minutes":60}'
cat data/vault/workouts/w-1.md # ...your edits survived the generated updateThat last step is the point. The generated update went through the runtime’s Document round-trip, which preserves unknown keys, key order, and the body. A field you added by hand is still there; an untouched file re-renders byte-for-byte.
The byte-identical demo
Section titled “The byte-identical demo”Everything above the store is identical between backends. You can check it directly:
diff -r ../iron-log/src-tauri/src/api/v1/generated src/api/v1/generatedZero output. Same API forwarding layer, same handler signatures, same everything — generated from the same schema against two completely different persistence layers.
The entire consumer delta
Section titled “The entire consumer delta”This is the complete list of what a consumer writes differently:
| iron-log (SeaORM) | iron-log-md (markdown) | |
|---|---|---|
Store holds | db: Arc<DatabaseConnection> | vault: markdown_store::VaultHandle |
| Generated CRUD calls | self.db() | self.vault() |
| Junction plumbing | sync_junction / load_junction_ids | none — many-to-many is a wikilink list in frontmatter |
AppError | DbError(String) | Md(String) + From<markdown_store::Error> |
| Storage | one SQLite file | data/vault/<entity>/<id>.md, editable anywhere |
And in build.rs, .seaorm(...) becomes .markdown_io(...):
Pipeline::new("src/schema") .markdown_io( "src/persistence/markdown/generated", MarkdownIoOptions { vault_root: "data/vault".into(), layout: MarkdownLayout::PerEntityDir, // Workout.name is Option<String>, so SlugFromField is out; // Provided keeps the example honest about where ids come from. id_strategy: IdStrategy::Provided, list_cap: 10_000, }, ) .dtos("src/schema/dto") .store("src/store/generated", Some::<PathBuf>("src/store/hooks".into())) .api("src/api/v1/generated", "AppState") .servers(servers_config) .clients(clients_config) .build()With exactly one persistence stage configured, the store backend is inferred — no .store_backend(...) call needed.
Two schema differences
Section titled “Two schema differences”The entities are otherwise identical to iron-log’s:
- Explicit
directory = "…"on each entity. The markdown backend uses it to pick the vault folder; SeaORM never read it. Without it the segment defaults to the singular type name and the store walks a folder your records aren’t in. IdStrategy::Providedrather thanSlugFromField.Workout.nameisOption<String>, so there’s no guaranteed field to slug from. Callers supply ids; an empty one is a runtime error.
Declared omission
Section titled “Declared omission”No TauriIpc transport generator. Compiling the Tauri stack for a headless demo buys nothing, and iron-log already exercises that path. The HTTP transport and the TypeScript client generators both run.
Where to look
Section titled “Where to look”build.rs— the one-stage swap, side by side with iron-log’s.src/store/mod.rs— the hand-writtenStore, to see how small the delta really is.src/store/generated/workout.rs— generated CRUD against the vault runtime instead of SeaORM.data/vault/workouts/— the records themselves. Open one in Obsidian.