# Obsidian Connector

**Two-way sync between an Obsidian vault and the Fabric graph — local, file-watcher driven, no plugin install.**

## What it does

Points the self-hosted Fabric daemon at one or more Obsidian vault folders. The connector:

* **Ingests** every `.md` note as a Fabric node — YAML frontmatter, body, `#tags`, and `[[wikilinks]]` extracted automatically.
* **Stays current** via a filesystem watcher (changes flow into Fabric within ~200 ms), with a 60 s interval poll as a fallback.
* **Emits back** when Fabric writes a note — atomic tempfile + rename, conflict-safe sidecar on concurrent external edits.
* **Tracks renames** by content-derived stable IDs (frontmatter `id:` if present, else SHA-256 of body) so wikilink targets survive moves.

No Obsidian plugin, no auth flow. The daemon reads the vault directory directly.

## Prerequisites

* Self-hosted Fabric daemon (`rsc-api-server`) running on the same host as the vault — local-host only in v1.
* Obsidian vault is a plain directory of `.md` files (no encrypted/iCloud-locked vaults).
* POSIX filesystem for atomic emit. macOS and Linux are first-class; Windows is best-effort (non-atomic rename across volumes).

## Configure

Set `OBSIDIAN_VAULTS` before starting the daemon:

```bash
export OBSIDIAN_VAULTS="research=/Users/me/Obsidian/Research,journal=/Users/me/Obsidian/Journal"
```

The value is a comma-separated list of `vault_id=path` pairs. The `vault_id` is a stable handle you choose — it appears in install IDs and the `provenance.source_id` of every ingested record.

Optional:

| Variable             | Default                                  | Purpose                                                                                  |
| -------------------- | ---------------------------------------- | ---------------------------------------------------------------------------------------- |
| `OBSIDIAN_TENANT_ID` | `0b51d1a4-0b51-4d1a-b51d-1a40b51d1a40`   | Tenant UUID for ingested records. Override when running multi-tenant.                    |
| `OBSIDIAN_USER_ID`   | (per-vault derived)                      | Pin every install to one user profile. **Single-vault only** — rejects 2+ vaults.        |

Bad UUIDs in either variable fail boot with a clear error. Bad paths in `OBSIDIAN_VAULTS` skip just that install and log a warning; other vaults still register.

## Verify

Start the daemon and check the boot log for one line per vault:

```text
INFO Obsidian per-install scheduler started (watcher + interval)
     install_id=… vault_id=research
```

Then drop a note in the vault — it should appear in the graph within a few seconds. Tags become tag edges, wikilinks become link edges, frontmatter fields land on the node.

## What gets ingested

For each `.md` file:

* **Frontmatter** — full YAML mapping kept on the node. Top-level sequences or scalars are rejected (the daemon logs and dead-letters the note rather than silently dropping the frontmatter).
* **Tags** — both YAML `tags:` and inline `#hashtag` syntax. Tags inside fenced ` ` \`\`\` or inline `` `code` `` are ignored.
* **Wikilinks** — `[[Target]]` and `[[Target|alias]]` become edges. Pipe-aliases are stripped; the target is canonical. Wikilinks inside code blocks are ignored.
* **Body** — the markdown after the frontmatter, verbatim.

Hidden directories (`.obsidian/`, `.git/`, `.trash/`) are skipped.

## Emit behavior

When Fabric writes a note back to the vault:

* The write goes to a tempfile in the same directory, then a `rename(2)` swaps it onto the canonical path — readers never see a partial file.
* If the canonical file's mtime has advanced past Fabric's last-seen value (something else changed it), the emit writes to `<basename>.conflict-<UTC-ts>-<rand>.md` instead. **No clobber.** Operators can grep `tracing` warns for `emit_conflict` to find them.
* A `rel_path` containing `..` or absolute path is rejected up front.

Dead-letter persistence for emit conflicts is deferred to v1.5 — the sidecar file IS the recovery surface in v1.

## Replay a backfill

If the cursor gets out of sync (e.g. you restored the vault from backup), drive a fresh backfill without restarting the daemon:

```bash
curl -X POST "http://localhost:3000/api/v1/connectors/installs/<install_id>/backfill"
```

The endpoint refuses if the install isn't `active`.

## Limitations

* **Local-host only.** Vault and daemon share a filesystem. Multi-tenant SaaS needs a per-tenant agent (deferred — see [issue #233](https://github.com/cognisos-ai/Prod_Fabric/issues/233)).
* **Last-writer-wins** on the canonical path; concurrent writes during the brief read-mtime → rename window can be overwritten silently. Acceptable for a single-user local vault.
* **No `PATCH /config`** for Obsidian installs — vault path is fixed at boot. Change `OBSIDIAN_VAULTS` and restart.

## See also

* [Fabric Indexer](/fabric-indexer) — the graph engine that stores ingested notes
* [MCP Reference](/mcp) — query the indexed vault from your AI client
