Vault as git projection — v0.7+ architecture

Date: 2026-05-20 Status: Architecture A shipped as of 2026-05-28. Landed across vault#380 (admin SPA git-mirror UI + manual-trigger endpoint), vault#382 (event-driven exports via HookRegistry + deletion handling + sync_mode schema reframe), and vault#383 (0.4.9-rc.5 release bump). The polling-shape v0.7-bootstrap from vault#346 has retired in favor of hook-driven exports. Bidirectional sync (Architecture B) and vault-as-thick-UI-over-git (Architecture C) remain deferred; this doc is preserved as the design record. Propagation tracked at parachute-patterns/migrations/2026-05-28-mirror-event-driven.md.

Companions:

The decision

v0.7+ moves vault toward being architecturally aware of its git projection. Vault remains the runtime (live SQLite + REST + MCP); a configured git mirror becomes the canonical at-rest store, the audit trail, and the portability surface. Like git's working tree vs .git store — inverted. Vault is the working tree clients edit; the git mirror is what survives the box.

The arc has three stations: (A) sidecar projection (vault → mirror, one-way, ships in v0.7), (B) bidirectional sync (mirror → vault flows back, ships if real demand materializes), (C) vault-as-thick-UI-over-git (git is canonical, SQLite is a runtime cache — deferred indefinitely). Ship A; defer B and C until operators are loudly asking.

The shape Aaron is asking for

Two threads converge here. Gitcoin Brain (vault-as-job-substrate per the "Gitcoin Brain (2026-05)" pattern — internal design notes) treats jobs as notes, runs as notes, and wants git history of the team brain for free — audit, time-travel, code-review-able diffs over decisions, commitments, run outputs. The existing portable-markdown export already gets them most of the way (see the vault-portable-export cookbook); they wire it via cron + shell loop today. Owner-operator export/import workflows (the broader pattern the cookbook codifies) want the same shape: vault as live store, git as the artifact you back up, diff, share, and read offline in Obsidian / IDE.

The friction the current setup leaves on the floor:

The end-state Aaron is gesturing at: vault has a mirror mode. Configure the mirror once; every write triggers an incremental export + commit (+ optional push). Eventually, edits to .md files in the mirror flow back into vault. Eventually, the SPA renders git history natively.

What's already built vs what's new

Already shipped

The portable-markdown export is mature and lossless. The vault-portable-export cookbook is authoritative; the headline shape:

This is the primitive everything below builds on. Re-architecting around git as canonical (option C) would discard this; building on top of it (options A + B) treats it as the load-bearing substrate.

Ships in the parallel PR (vault#346)

A small step from "shell loop + cron" toward "vault knows about its mirror":

This stays inside the CLI surface. No schema changes, no hub-side config, no UI surface. It validates the timing model (polling re-export on a fixed interval) before committing to architectural awareness — and surfaces the cost (a 5s upper bound on projection latency, every interval whether anything changed or not) that Architecture A's event-driven post-write hooks improve on.

What THIS design doc covers (v0.7+)

The next architectural step — vault becomes config-bound to its mirror, not external-CLI-bound:

Architectural options

Three increasingly-ambitious shapes. Each is a strict superset of the prior.

A — Sidecar projection (smallest delta)

Vault writes trigger export to a configured mirror directory. The configuration is bound — vault reads it on boot, registers post-write hooks, runs incremental re-exports through the existing --since machinery. Auto-commit (and optional auto-push) shells out to git.

This refinement adds two axes of operator choice (location + watch mode) while keeping the architecture A-shaped. The mirror is still a one-way projection; the underlying data flow doesn't change.

This is the "vault mirrors itself" pattern. It operationalizes what cookbook readers wire by hand today.

Two axes of operator choice

A's surface area opens along two independent axes:

All four combinations are technically valid; the system surfaces three as presets.

Three preset modes

When the operator enables vault-sync, they pick from three presets. The form pre-fills location + watch + commit settings to match.

Preset 1 — "History" (internal + auto-watch). Vault auto-exports to a hidden mirror at ~/.parachute/vault/data/<name>/mirror/. Always-on; the mirror stays current. The operator doesn't think about it and never sees it unless they go looking. Use case: "give me an audit trail without operating overhead." This is the lowest-friction option — one click, no path picker, no permissions to worry about.

Preset 2 — "Live Mirror" (external + auto-watch). The operator picks a path (~/Documents/my-vault-mirror/, etc.). Vault keeps it live via the watch loop. Optional auto-commit + auto-push for the GitHub-backed scenario. Use case: "I want to push to GitHub, open in Obsidian, or share with my team." The visible-folder + always-current shape that matches the cookbook recipe most operators wire by hand today.

Preset 3 — "Manual Export" (external + manual). The operator picks a path. No auto-watch; they run parachute-vault export --vault <name> when they want a fresh snapshot. Optional auto-commit on each manual export. Use case: "I want explicit control over when exports happen." For operators who'd rather batch exports (end-of-day snapshot, pre-meeting freeze, etc.) than have a process polling in the background.

The fourth combination — internal + manual — is technically supported but isn't surfaced as a preset. Internal mirrors get most of their value from being always-current; if an operator wants manual control, external is the natural shape (they can browse the result). Advanced operators can still configure it via the raw config form if a use case shows up.

The defaults framing is opt-in always. No mirror exists until the operator explicitly enables vault-sync. When they do, the three presets cover the common cases; the raw config form covers the rest.

B — Bidirectional sync (real two-way)

A filesystem watcher (the same primitive options A uses, extended to read events) watches the mirror dir for .md file changes. On a change: parse the markdown via the existing import machinery, diff against in-vault state, apply changes. Conflict resolution becomes load-bearing.

This is the "edit your notes in Obsidian" pattern. It assumes the operator is the same person editing in both places; cross-tenant bidirectional sync is a different problem (see "What this doesn't cover").

C — Vault-as-thick-UI-over-git (the deepest model)

Git repo is the canonical store. SQLite is a runtime cache, rebuilt on demand from a git checkout. The operations log becomes git commits. parachute-vault clone <git-url> becomes the install method; git pull becomes vault sync; conflicts resolve via git merge semantics.

This is the "vault is just a fancy git client" model. Powerful — and a different product. Cite Foam, Logseq's git mode, the git-bug model as priors. Don't commit to it; reserve it as a possibility.

Recommended path

Ship A. Defer B behind demand signal. Don't commit to C.

The pattern this follows is the same one trust-gradient-isolation.md named for runtime primitives: name the audience first, ship the lightest viable thing for them, don't span audiences in one primitive. A is the lightest thing that closes Aaron's stated friction. B is a different audience (the Obsidian-first editor). C is a different product entirely.

Concrete v0.7 implementation (architecture A in detail)

Schema additions

One new hub_settings key per vault, namespaced by vault name. The shape carries both axes (location + watch mode) plus the commit/push settings:

// hub_settings table — one row per vault that has a mirror configured
{
  "key": "vault_mirror_<name>",       // e.g. "vault_mirror_gitcoin"
  "value": {
    "enabled": true,
    "location": "internal",           // "internal" | "external"
    "external_path": null,            // path string when location === "external", null when internal
    "watch": false,                   // true = auto-watch loop runs; false = manual export only
    "auto_commit": true,
    "auto_push": false,
    "commit_template": "export: {{date}} ({{notes_changed}} note{{plural}})"
  }
}

(commit_template above is the default; the admin SPA help text lists {{change_summary}} and other variables operators can include for richer commit messages — see the vault-side variable list further down.)

Field semantics:

The presets map to these fields as:

Preset location external_path watch auto_commit (default) auto_push (default)
History internal null true true false
Live Mirror external operator-supplied true true false (toggle to enable for GitHub-backed flow)
Manual Export external operator-supplied false true false

The wiring decision (vault config vs hub_settings) is open question 5 below. The shape above assumes hub_settings; if vault config wins, the same JSON moves under a mirror: block in vault's config.yaml. The fields are identical either way.

Vault-side

Hub-side admin UI

A new admin SPA page at /admin/vault-mirrors. Mirrors the multi-user phase-1 /admin/users pattern in chrome + interaction shape.

The three presets are the primary entry point. Operators pick a preset; the form pre-fills location + watch + commit settings. Advanced operators can drop into the raw config to customize (or land on the fourth combination — internal + manual).

Surface:

UX considerations

Conflict resolution (option B — if and when we ship it)

The trade-offs the bidirectional case forces a pick on:

Recommendation for v0.7.5: LWW with audit log. Cheapest to ship; matches the audience (one operator editing in both places, conflicts mostly transient — say, "I edited in vault while my offline laptop was still queued to push"). If loss-of-work complaints accumulate, escalate to a conflict UI. Three-way merge is overkill for the v0.7.5 audience.

The audit log lives in vault as tag:conflict-resolution notes (one note per conflict, with metadata.original, metadata.overridden, metadata.winner). This is recursive but appropriate: vault's audit shape is "audit lives in vault," same as the run-output convention Gitcoin Brain uses.

UI history surface (v0.7+)

Once the mirror is git-backed, vault SPA can render git history natively:

Implementation: hub queries the mirror git repo via git log --follow <file> (handles path renames as long as git's rename detection catches them), parses the output, renders. No new persistent storage — the git repo is the storage.

This surface is gated on a mirror being configured. Vaults without mirrors have no history tab. The UI degrades gracefully — "Configure a git mirror in the admin SPA to see note history" — not "broken feature."

Could ship in v0.7 alongside A or split into v0.7.5 (open question 3). Splitting is the safer ship; the history surface is "nice to have" while the mirror itself is the load-bearing primitive.

Multi-vault layouts

Two operator preferences, both supportable:

Support both. The schema is just a path — vault doesn't care whether the path is a repo root or a subdirectory of a larger repo. The admin SPA's path validation accepts either ("is this directory inside a git repo?") and exposes the resolved repo root in the status display so the operator knows what they're committing to.

No opinion at the schema level. The cookbook entry grows two recipes — "one repo per vault" and "monorepo" — and points to each from the admin SPA's help text.

What this doesn't cover

Trade-offs to flag for review

Open questions for Aaron

These need an explicit call before any code starts:

  1. A vs B vs C as the v0.7 ship. Recommended A, but the call is yours. B is a real audience (Obsidian-first editors) and might be worth more weight than this doc gives it; C is the most opinionated and would reshape vault's identity (worth doing if right, costly if wrong).
  2. Multi-vault default layout. Per-vault repos (clean separation) or monorepo (easier to manage many vaults)? The schema supports both; the question is which one the admin SPA's "create new mirror" wizard defaults to, and which the docs lead with.
  3. UI history in v0.7 or split to v0.7.5. History surface is gated on A landing; we can ship them together or land A first + UI second. Splitting is safer; shipping together is more cohesive.
  4. Bidirectional sync as a roadmap item (vs maybe-someday). Should B be on the public roadmap (signaling intent to ship) or in the "if demand materializes" bucket (the door is open but no promise)? The framing affects whether early-adopter operators design their workflows around it.
  5. Mirror config — vault config or hub_settings. Vault config keeps the mirror knowledge co-located with the vault (move the config.yaml to a new box, the mirror follows). Hub_settings keeps it administratively centralized (one place to see all mirrors, easier for the multi-user case where the admin manages per-user vault mirrors). The decision is a coupling choice: is the mirror an operational concern (hub_settings) or a vault-level configuration (vault config)? The schema is identical either way; the question is which surface owns it. Current lean: hub_settings — the admin SPA is where the operator is already managing user-vault assignments, and centralizing mirror config there matches the multi-user phase-1 shape.
  6. Preset mode change — one-click flip or full re-entry? Switching an existing mirror from one preset to another (e.g. "Manual Export" → "Live Mirror") could be a single confirmed click that flips watch: falsewatch: true and keeps the same path, or it could force the operator to re-enter all settings as if configuring a new mirror. Lean: one-click flip with confirmation — the presets are independent axes, so the path doesn't change when only watch flips, and full re-entry would add friction for no safety gain. Full re-entry only if the operator wants to also change external_path (or location) simultaneously; that's a different flow ("change where this mirror lives") and deserves the more deliberate UX.

What this changes about earlier docs

Nothing in 2026-05-18-v06-deploy-architecture.md changes — the mirror lives on the same persistent disk hub already manages (/parachute/mirrors/<vault>/). Nothing in 2026-04-20-module-architecture.md changes — the mirror is internal to vault, not a new module. The vault-portable-export cookbook grows a new recipe ("hub-managed mirror") once A ships, and the existing "nightly git projection" recipe becomes the bootstrap path for operators on pre-v0.7 hubs.

The multi-user phase-1 design (2026-05-20-multi-user-phase-1.md) gets a small future ripple: the admin SPA's /admin/vault-mirrors page sits next to /admin/users, and the "assign vault to user" flow can grow a "with mirror configured" badge so the operator sees at a glance which user-vaults are git-backed. Optional and not v0.7-gating.

Note: git_branch is dropped from the v0.7 ship. Earlier prototypes of the config shape carried a git_branch: "main" field; the refined shape removes it. Branch handling for git push defaults to the repo's current branch (git push with no refspec), which matches the cookbook's existing mental model. Explicit branch targeting can return as a Phase 2 polish if real usage shows operators want to push to a non-current branch.

Why the architecture is right

The criterion that locks A in: the existing portable-markdown export is the load-bearing primitive, and A is the smallest config-bound wrapper around it. Everything that's already deterministic about the export — fixed key order, byte-equivalent re-emit, lossless round-trip — applies to A's commits unchanged. The git diffs stay clean. The round-trip stays lossless. The cookbook readers' mental model stays intact; the only thing that changes is "you don't have to run cron anymore."

B and C re-derive useful properties (real two-way editing, git as canonical) at significant cost. They're real audiences; they're just not the audience Aaron's current friction is calling out. Shipping A first keeps the door open for both without committing to either prematurely. That's the same shape the trust-gradient-isolation pattern names at the runtime layer, applied here at the storage layer: name the audience, ship the lightest thing, don't try to span audiences in one primitive.