10.0 KB · updated 2026-07-06 · md

obsidian-sync.md

docs/integrations/obsidian-sync.md

Obsidian bidirectional sync — proposed design

<!-- translations:start -->

한국어 · 中文 · 日本語 · Русский · Español · Français · Deutsch

<!-- translations:end -->

Status: Shipped (Tier 1, v0.5.0). The overlay reader, user-notes append zones, watch mode, and orphan pruning described below are live behind tesserae vault sync. This page doubles as the design rationale and the user guide. Multi-vault federation (Tier 3) remains out of scope.

The Obsidian export used to be strictly one-way: the typed graph in .tesserae/graph.json projects to the vault, and project compile overwrites projected files. obsidian-sync adds the opposite direction — edit a description in Obsidian, and it survives recompile.

This document spells out how that works without making the data model incoherent.

Strategic shift, stated plainly

The current README disclaims live editing:

Tesserae picks compile-from-source over live editing. If you want to edit notes in a UI, use Logseq or Obsidian.

Bidirectional sync changes that contract for a subset of fields. Worth being deliberate. The goal is not "Obsidian becomes the editor" — it's "the user's Obsidian edits aren't silently destroyed on recompile".

The core idea: overlays, not merges

Rather than trying to merge two diverging copies of the same node, treat the vault as a diff layer over the projection:

source markdown  ──extract──▶  base_graph
                                    +
                              vault_overrides     ◀── computed from vault
                                    ↓
                              final_graph  ──project──▶  vault (.md files)

vault_overrides.json lives in .tesserae/ and is computed, not authored. On each compile, Tesserae walks the vault, compares each projected page against what the previous projection wrote, and records every user-introduced change as an overlay entry. The final graph is base_graph with overlays applied. The next projection writes the result back to disk.

Round-trip stable. Recompiling the same vault with no source-side changes produces no diffs.

Per-field ownership

Each field on a node has an owner. Ownership decides what happens when source and vault disagree.

FieldSource-ownsVault may overrideNotes
id, typeyesnoSchema-controlled; extractor-owned
nameinitialyesUser often knows canonical name better than the extractor
aliasesinitialyesAppend-only from vault; vault entries always preserved
descriptioninitialyesThe most common Obsidian edit
source_pathyesnoProvenance; can't be edited away
metadata (declared keys)initialyesE.g. arxiv_id, github_repo — user can correct
metadata.user.*n/ayesReserved namespace for user-only keys; extractor never writes
Outgoing edges (typed)yesnoEdges live in the ontology, not the vault
New wikilinks the user typesn/ayesSurfaced as edge_type=user_link, written to graph
<!-- user-notes --> body blocknever writtenalways preservedAppend-only zone the projector never touches

Conflict cases and defaults

CaseDefaultWhy
Vault description differs from re-extracted source descriptionVault wins, log to .tesserae/lint-report.md under "diverged fields"User-edit-respects: the user clearly intended the edit. Audit trail lets you review later.
Source file deleted, projected page still in vaultRemove node from graph, list in .tesserae/orphans.mdSource is authoritative for existence; orphan log lets you decide whether to restore or accept
User wrote a wikilink to a slug that doesn't existCreate tombstone node (type Stub), surface in lint reportDon't drop the user intent; flag it for cleanup
User added a frontmatter key the schema doesn't knowPreserve as metadata.user.<key>, never overwriteForward-compatible without polluting the typed graph
Two vaults on different machines edit the same node, both synced via Obsidian SyncOut of scope for v1. Last-writer wins at the filesystem level.True multi-vault federation is Tier 3; defer until a real use case

User-notes append zone

Every projected page gets a fenced zone the projector never touches:

> [!quote] Paper
> Headline contribution and method sketch projected from the graph...

<!-- user-notes:start -->

Your notes here. Anything between the markers survives recompile forever.
Wikilinks here become `user_link` edges in the graph on the next pull.

<!-- user-notes:end -->

## Outgoing
- ...

Two practical effects:

  1. Users can annotate any page (e.g. "see chapter 4 of my notes") without losing it on rebuild.
  2. The pull pass scans the user-notes block for wikilinks and surfaces them as ontology-typed user_link edges, giving them graph reachability without polluting the formal edge types.

Remote transport — explicit non-goal

Tesserae does not build a sync server, auth layer, conflict-resolution daemon, or hosted vault. "Bidirectional" here means "compile reads from the vault" — what gets the vault to the machine doing the compile is the user's problem, solved by tools that already exist:

StackCostNotes
Obsidian SyncPaid, $4-8/moE2E-encrypted, official, dead simple
iCloud / Dropbox / OneDriveBundled with the OSWorks but conflict UX is hostile
SyncthingFree, self-hostedBest for solo cross-device
Git (vault committed)FreeConflict UX is best for technical users
LiveSync (CouchDB plugin)Free, requires serverReal-time multi-device

All five are compatible with the overlay model because Tesserae sees the vault as files-on-disk, not as a stream of mutations.

CLI surface

tesserae vault sync applies vault edits onto the typed graph and re-projects:

# Apply the overlay once: pull user edits, re-project to the vault.
tesserae vault sync

# Inspect what would change first. Writes .tesserae/diverged-fields.md and
# does NOT apply or re-project.
tesserae vault sync --dry-run

# Point at a specific vault for this call (resolution order:
# --vault > config.obsidian.vault_path > .tesserae/obsidian_vault/).
tesserae vault sync --vault ~/Documents/tesserae-vault

# Make that vault path the default for future commands.
tesserae vault sync --vault ~/Documents/tesserae-vault --persist-vault

# Long-running watch: re-apply the overlay every time the vault changes.
# Ctrl-C to stop; tune the poll cadence with --poll-interval (default 1.5s).
tesserae vault sync --watch --poll-interval 1.5

# Delete projected pages whose source node no longer exists (the projector
# only overwrites, never deletes). Pages with user-notes are kept unless you
# also pass --force-prune-with-notes.
tesserae vault sync --prune-orphans
tesserae vault sync --prune-orphans --force-prune-with-notes

The /tesserae:obsidian-sync slash command wraps this, and tesserae refresh (plus the /tesserae:refresh macro) runs the overlay as the last step of its import → compile → sync chain.

Delivery status

TierScopeStatus
1aOverlay reader: walk vault, build vault_overrides.json, apply at sync. Divergences land in .tesserae/diverged-fields.md.Shipped
1bUser-notes append zones: projector never touches <!-- user-notes:start --> ... <!-- user-notes:end --> blocks.Shipped
2Watch mode: long-running obsidian-sync --watch re-runs the overlay on a poll loop as the vault changes.Shipped
3Multi-vault federation: graph stores per-vault provenance, supports concurrent edits across synced vaults.Deferred until a real use case

Non-goals (explicitly)

  • A sync server / auth / hosted backend.
  • Real-time collaborative editing inside Obsidian (use LiveSync if you need this).
  • Rewriting the extractor to round-trip every field — the source markdown stays canonical for everything outside the override table.
  • Sync of the static HTML site (build-site remains projection-only).

Resolved decisions

These were the open questions at design time; the shipped Tier 1–2 implementation settled them as follows:

  1. Lint report shape. Diverged fields surface as a dedicated .tesserae/diverged-fields.md file (written by --dry-run and on every apply) so it can be diffed in git, rather than as a section of lint-report.md.
  2. Tombstone node type. Add Stub as a real schema type, or piggyback on OpenQuestion with a _kind: stub discriminator? Proposed: real type, named Stub, hidden from public indexes.
  3. Pull-on-compile default. Default ON or default OFF? Proposed: ON when a vault exists at the configured path, with a one-time confirmation prompt the first time it activates so users opt-in deliberately.
  4. What counts as "the previous projection" for diffing? Snapshot stored in .tesserae/vault_snapshot.json, or re-project on the fly each compile? Proposed: snapshot, written at end of every compile. Cheaper and avoids extractor non-determinism leaking into the overlay.
  5. Multi-language vault projection. Today's projection is single-language (the source). Should overlays be locale-aware (e.g. an edit to description in a Korean vault overlay applies only to the Korean projection)? Proposed: out of scope for v1; vault is single-language matching the project's primary language.

How this shows up in obsidian.md

The user-facing guide stays focused on "you can read and query the vault", then links here for the round-trip story with a one-line summary: "Edit fields in Obsidian, they survive recompile. See obsidian-sync.md for the full model."