Skip to main content
← BACK TO WRITING
11 MIN READ INTERMEDIATE Published June 30, 2026, 11 min read

Building an AI-Powered Second Brain: Obsidian + Syncthing + Tailscale + Ollama

My Obsidian vault was growing but barely usable — auto-commits polluting my GitHub history every five minutes, a flat folder structure that made finding anything a chore, and no way to actually query what I knew. This is the complete guide to how I fixed all of it: a homelab server as the sync hub, Tailscale for remote access, a PARA-based vault structure with smart templates, live Dataview dashboards, and a local AI assistant that answers questions about my notes in under 10 seconds — without sending a single byte to any cloud service.

#OBSIDIAN #SECOND-BRAIN #SELF-HOSTED #AI/ML #HOMELAB #PRODUCTIVITY
bashpythonyaml
Building an AI-Powered Second Brain: Obsidian + Syncthing + Tailscale + Ollama

My Obsidian vault was committing itself to GitHub every five minutes. Hundreds of entries like vault backup: 2026-06-15 14:32:07, vault backup: 2026-06-15 14:37:11 — meaningless noise that buried the commits that actually mattered. The vault had grown to 60+ notes scattered across a flat folder structure that made finding anything a manual scroll. And when I wanted to ask "what am I working on right now?" I had to open three different files.

I knew what I wanted: seamless sync across my work laptop, personal laptop, and phone — without the git noise. A structure that guided me instead of fighting me. And some way to actually talk to my notes and get useful answers back.

This post is the complete guide to how I built it. Everything here is free (except your hardware), fully self-hosted, and runs permanently on a homelab Ubuntu server.


The Architecture

Before touching any config file, it helps to understand what we're building. The whole system follows one rule: the homelab is the hub. Every device talks to it — not to each other.

Text
                    ┌─────────────────────────────────────┐
                    │              HOMELAB                │
                    │     Always on · source of truth     │
                    │                                     │
                    │  Obsidian vault                     │
                    │  Syncthing (sync server)            │
                    │  Ollama (local AI on GPU)           │
                    │  Git → nightly push to GitHub       │
                    └──────────────────┬──────────────────┘

                               Tailscale VPN
                         (private mesh — any network)

         ┌─────────────────────────────┼─────────────────────────────┐
         │                             │                             │
    Work laptop                 Personal laptop                    Phone
         └─────────────────────────────┴─────────────────────────────┘
                          Syncthing — real-time sync

What each piece does

Four tools, four jobs. None of them replace the others — they stack on top of each other.

Syncthing — real-time sync (homelab + every device)

Syncthing is the conveyor belt that moves your vault between devices. Save a note in Obsidian and Syncthing detects the change within seconds, then pushes it to the homelab. Unlike git auto-commit — which creates a new commit every few minutes and buries your history in vault backup: timestamp noise — Syncthing just moves files. Silent, fast, no commit messages. Every device runs Syncthing, but they all talk to the hub, not to each other.

Tailscale — reach the homelab from anywhere (every device)

Your homelab sits on your home network. Your work laptop does not. Tailscale bridges that gap by putting every device on a private encrypted mesh. Each one gets a stable 100.x.x.x address that works from the office, a café, or mobile data — as if the homelab were on the same Wi-Fi. No port forwarding, no dynamic DNS, no exposing your home IP to the internet.

Git + GitHub — nightly offsite backup (homelab only)

Syncthing handles speed; git handles safety. Every night at 2 AM, the homelab takes a snapshot of the vault and pushes it to a private GitHub repo. If the server dies, you git clone and you're back. If you delete a note by mistake, git checkout restores it. This runs only on the hub — your laptops never commit anything. One meaningful backup per day, not hundreds of auto-commits.

Ollama — local AI on your notes (homelab GPU)

The AI runs where the vault lives. Ollama serves language models on the homelab's GPU. When you ask Smart Composer a question from your laptop, the request travels over Tailscale to the homelab, reads your actual notes, and returns an answer. Your data never touches OpenAI, Anthropic, or any cloud API.

How a note travels

Picture this: you're at the office and capture a meeting action item in Obsidian.

  1. You save the note on your work laptop. Syncthing detects the new file immediately.
  2. The change reaches the homelab within seconds — over Tailscale if you're off the home network, or directly over LAN if you're at home.
  3. Your personal laptop picks it up the next time you open Obsidian at home. Your work laptop can be shut down and packed away. Only the homelab needs to stay online for sync to keep working.
  4. That night at 2 AM, the homelab commits the day's changes in a single git snapshot and pushes to GitHub.

Devices never sync directly with each other. Every change flows through the hub first. That makes the system easier to reason about: one machine is always on, always holds the latest copy, and is reachable from anywhere over Tailscale.


Part 1 — Sync Infrastructure

Why Syncthing instead of git auto-commit

Git auto-commit creates a commit for every save, every few minutes. After a week you have hundreds of vault backup: timestamp entries that make git log useless for anything meaningful. Syncthing syncs file changes at the byte level, silently, in the background. Your git history stays clean for intentional snapshots — architecture decisions, project milestones, weekly reviews.

Syncthing also handles the hard edge cases automatically: it discovers devices on the local network in milliseconds via UDP broadcast, falls back to relay servers when direct connections are blocked, and keeps both versions of a file if you edit it simultaneously on two offline devices — a .sync-conflict- file appears for you to review.

Installing Syncthing on your Ubuntu homelab

Shell
# Install prerequisites
sudo apt update
sudo apt install -y curl gpg apt-transport-https
 
# Add official signing key
sudo mkdir -p /etc/apt/keyrings
curl -s https://syncthing.net/release-key.txt \
  | gpg --dearmor \
  | sudo tee /etc/apt/keyrings/syncthing-archive-keyring.gpg > /dev/null
 
# Add stable repository
echo "deb [signed-by=/etc/apt/keyrings/syncthing-archive-keyring.gpg] \
https://apt.syncthing.net/ syncthing stable" \
  | sudo tee /etc/apt/sources.list.d/syncthing.list
 
sudo apt update
sudo apt install -y syncthing

Run Syncthing as your normal user (not root) via systemd:

Shell
# Replace your_username with your actual Linux username
sudo systemctl enable syncthing@your_username.service
sudo systemctl start syncthing@your_username.service
sudo systemctl status syncthing@your_username.service
# Expected: Active: active (running)

Create the vault directory and note your Device ID — you will need it when pairing devices:

Shell
mkdir -p /home/your_username/my-vault
syncthing --device-id
# Output: MFZWI3D-BONSGYC-YLTMRWW-...  ← save this

Access the web UI from your laptop via SSH tunnel (the homelab is headless):

Shell
# Run this on your laptop
ssh -L 8384:localhost:8384 your_username@192.168.1.100
# Then open: http://localhost:8384

Set a strong password immediately — Actions → Settings → GUI tab → Authentication.

Installing Syncthing on Ubuntu laptops

Identical process on each laptop:

Shell
sudo apt update && sudo apt install -y curl gpg apt-transport-https
 
sudo mkdir -p /etc/apt/keyrings
curl -s https://syncthing.net/release-key.txt \
  | gpg --dearmor \
  | sudo tee /etc/apt/keyrings/syncthing-archive-keyring.gpg > /dev/null
 
echo "deb [signed-by=/etc/apt/keyrings/syncthing-archive-keyring.gpg] \
https://apt.syncthing.net/ syncthing stable" \
  | sudo tee /etc/apt/sources.list.d/syncthing.list
 
sudo apt update && sudo apt install -y syncthing
sudo systemctl enable syncthing@$USER.service
sudo systemctl start syncthing@$USER.service

Open http://localhost:8384 and note the Device ID:

Shell
syncthing --device-id

Setting up Tailscale for remote access

Syncthing can use relay servers when devices are on different networks, but relays add latency and a cloud dependency for every sync operation. Tailscale creates a private WireGuard mesh — every device gets a stable 100.x.x.x IP that works regardless of which physical network it's on, punches through firewalls, and requires zero port forwarding on your router.

Install on every device — homelab, both laptops, phone:

Shell
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up
# Open the printed URL, log in with the same account on every device

Get your homelab's Tailscale IP — you'll use this address everywhere:

Shell
tailscale ip -4
# Returns: 100.x.x.x  ← save this

Verify from a laptop:

Shell
tailscale ping 100.x.x.x
# Expected: pong from homelab in Xms

Pairing devices in Syncthing

On the homelab Syncthing UI (via SSH tunnel):

  1. Click Add Remote Device → paste your laptop's Device ID → name it laptop-work
  2. Repeat for each device
  3. Click the vault folder → EditSharing tab → check all devices
  4. Folder Type: Send & Receive on all

On each laptop's Syncthing UI at http://localhost:8384:

  • Accept the incoming connection request from the homelab
  • In the device settings, set the address to tcp://100.x.x.x:22000 (your Tailscale IP)
  • Accept the folder share, set the local path to your vault location
  • Folder Type: Send & Receive

Android sync

Install Syncthing-Fork from F-Droid or the Play Store. Use Syncthing-Fork specifically — not the original Syncthing app. It has better battery optimization and background sync reliability.

  • Grant Storage permission when prompted
  • Add homelab as a device: its Device ID + address tcp://100.x.x.x:22000
  • Accept the folder share, set path to /storage/emulated/0/my-vault
  • Critical: Android Settings → Apps → Syncthing-Fork → Battery → Unrestricted
  • Enable "Start on boot" and "Run in foreground" in the app settings

Create .stignore at vault root

Excludes device-specific Obsidian state so workspace layouts don't overwrite each other across devices:

Shell
cat > /path/to/my-vault/.stignore << 'EOF'
.obsidian/workspace
.obsidian/workspace.json
.obsidian/workspace-mobile.json
.obsidian/cache
.obsidian/.cache
.DS_Store
Thumbs.db
desktop.ini
*~
*.tmp
*.swp
EOF

Disable obsidian-git auto-commit

If you have obsidian-git installed, turn off auto-commit — Syncthing handles real-time sync now. Keep the plugin for manual intentional snapshots:

Shell
cat > /path/to/my-vault/.obsidian/plugins/obsidian-git/data.json << 'EOF'
{
  "autoSaveInterval": 0,
  "autoPushInterval": 0,
  "autoPullInterval": 0,
  "autoPullOnBoot": false,
  "disablePopupsForNoChanges": true,
  "showStatusBar": true,
  "pullBeforePush": true,
  "syncMethod": "rebase"
}
EOF

Verify end-to-end sync

Shell
# On your work laptop — create a test note
echo "sync test $(date)" > /path/to/vault/_sync-test.md
 
# On homelab — check it arrived within 10 seconds
ssh your_username@192.168.1.100 "cat /home/your_username/my-vault/_sync-test.md"
 
# Clean up
rm /path/to/vault/_sync-test.md

Part 2 — Vault Structure

The PARA system for developers

PARA (Projects, Areas, Resources, Archive) by Tiago Forte organizes notes by actionability rather than topic. I adapted it for a developer's context — an active side project, homelab documentation, learning notes, and daily captures — using numbered prefixes to enforce sidebar sort order.

Text
my-vault/
├── 00-Inbox/              ← everything new lands here first
├── 00-Home/               ← navigation hubs
│   ├── Home.md            ← morning dashboard
│   └── Projects-MOC.md   ← projects overview

├── 10-Projects/           ← active side projects
│   ├── my-app/
│   │   ├── Specs/         ← feature specs and requirements
│   │   ├── Decisions/     ← Architecture Decision Records
│   │   └── Sprints/       ← sprint notes
│   └── another-project/

├── 20-Areas/              ← ongoing responsibilities
│   ├── Homelab/           ← server and infrastructure docs
│   ├── Learning/          ← courses, books, research
│   └── Finance/           ← personal finance tracking

├── 30-Resources/          ← reference material
│   ├── Dev/               ← code snippets, patterns
│   ├── Prompts/           ← AI prompt templates
│   └── Snippets/          ← reusable code blocks

├── 40-Archive/            ← completed or inactive content
├── _Templates/            ← all Templater templates
└── _attachments/          ← all images land here automatically

The numbered prefixes enforce sidebar order — Inbox always appears first, Archive always last. The underscore prefix on _Templates and _attachments pushes them above numbered folders, visually separated from content.

Fix Obsidian's default paths

Two settings to change immediately:

Attachments — so pasted images stop landing at the vault root:

Text
Settings → Files & Links
→ Default location for new attachments
→ In the folder specified below → _attachments

New notes — so everything lands in Inbox by default:

Text
Settings → Files & Links
→ Default location for new notes
→ In the folder specified below → 00-Inbox

Part 3 — Plugins and Configuration

The plugin stack

Nine plugins, but they fall into four clear layers. Install them all via Settings → Community Plugins → Browse.

Structure — make every note consistent without thinking about it

  • Linter — Runs on every save. Adds created and updated dates, sorts frontmatter keys, and cleans up formatting. This is what makes your notes queryable later — without it, Dataview and the AI have nothing reliable to search on.
  • Templater — Fires the right template when you create a note in a specific folder. Press Ctrl+N inside 10-Projects/my-app/Decisions/ and an ADR template appears with today's date, the right metadata, and your cursor in the right section. No copy-paste, no manual frontmatter.

Discovery — find anything in seconds

  • Dataview — Turns your vault into a queryable database. Powers the Home.md dashboard: live lists of inbox items, active projects, recent decisions. Updates automatically as you write — no manual curation.
  • Omnisearch — Full-text search across every note, including content inside PDFs and images. Faster and more thorough than Obsidian's built-in search. This is what you reach for when you know you wrote something but can't remember where.

Tasks — track work across the vault

  • Tasks — Finds every - [ ] checkbox in your vault and lets you query them by due date, project, or tag. Works inside any note — no separate task app.
  • Kanban — Visual board for a single project's TODOs. Each column is a markdown file, so it syncs like everything else. Good for sprint planning inside a project folder.

AI — talk to your notes

  • Smart Composer — Chat panel inside Obsidian that reads your open notes as context and sends questions to Ollama on the homelab. Ask "what are the open questions in this spec?" and get an answer grounded in your actual content — not a generic LLM response.

Extras — nice to have, not required for the core setup

  • Excalidraw — Embed whiteboard diagrams directly in notes. Useful for architecture sketches and flowcharts.
  • obsidian-git — Manual git snapshots from inside Obsidian. Auto-commit stays disabled (Syncthing handles sync; the homelab handles nightly backups). Use this for intentional commits at milestones — end of a sprint, after a restructure.

Configuring Linter for automatic frontmatter

Linter is the foundation. It runs silently on every save — adding created and updated dates automatically, sorting frontmatter keys in a consistent order, and cleaning up whitespace. You never think about dates or formatting again.

Shell
cat > /path/to/my-vault/.obsidian/plugins/obsidian-linter/data.json << 'EOF'
{
  "ruleConfigs": {
    "yaml-timestamp": {
      "enabled": true,
      "date-created": true,
      "date-created-key": "created",
      "date-created-source-of-truth": "file system",
      "date-modified": true,
      "date-modified-key": "updated",
      "date-modified-source-of-truth": "file system",
      "format": "YYYY-MM-DD",
      "update-on-file-contents-updated": "always"
    },
    "yaml-key-sort": {
      "enabled": true,
      "yaml-key-priority-sort-order": "type\ndomain\nstatus\nproject\ntags\ncreated\nupdated",
      "priority-keys-at-start-of-yaml": true,
      "yaml-sort-order-for-other-keys": "None"
    },
    "add-blank-line-after-yaml":     { "enabled": true },
    "format-tags-in-yaml":           { "enabled": true },
    "trailing-spaces":               { "enabled": true },
    "space-after-list-markers":      { "enabled": true },
    "empty-line-around-code-fences": { "enabled": true },
    "heading-blank-lines":           { "enabled": true, "bottom": true }
  },
  "lintOnSave": true,
  "suppressMessageWhenNoChange": true,
  "displayChanged": false,
  "foldersToIgnore": ["_attachments", "40-Archive"]
}
EOF

The frontmatter schema

Consistent frontmatter makes notes queryable by both Dataview and AI. Every note follows this schema:

YAML
---
type: note | spec | decision | resource | journal | inbox | moc
domain: project | homelab | learning | personal
status: active | draft | unprocessed | done | archived
project: my-app            # optional — links note to a project
tags: [tag1, tag2]
created: 2026-06-29        # auto-added by Linter
updated: 2026-06-29        # auto-updated by Linter on every save
---

The type tells Dataview and the AI what kind of note this is. The domain separates projects from personal areas. Together they enable precise queries like "show me all active decisions for my-app" without any manual tagging effort.

Backfilling frontmatter on existing notes

If you have existing notes with no frontmatter, this Python script adds sensible defaults based on folder location — run it once:

Python
import os
from datetime import datetime
from pathlib import Path
 
VAULT = Path("/home/your_username/my-vault")
 
FOLDER_DEFAULTS = {
    "10-Projects":         {"type": "spec",      "domain": "project"},
    "10-Projects/my-app/Specs":      {"type": "spec",      "domain": "project", "project": "my-app"},
    "10-Projects/my-app/Decisions":  {"type": "decision",  "domain": "project", "project": "my-app"},
    "10-Projects/my-app/Sprints":    {"type": "note",      "domain": "project", "project": "my-app"},
    "20-Areas/Homelab":    {"type": "note",      "domain": "homelab"},
    "20-Areas/Learning":   {"type": "resource",  "domain": "learning"},
    "20-Areas/Finance":    {"type": "note",      "domain": "personal"},
    "30-Resources":        {"type": "resource",  "domain": "personal"},
    "00-Inbox":            {"type": "inbox",     "domain": "personal"},
}
 
SKIP_FOLDERS = ["_Templates", "_attachments", "40-Archive", ".obsidian", "00-Home", ".git"]
 
def has_frontmatter(content):
    return content.startswith("---\n") and content.find("\n---\n", 4) != -1
 
def get_defaults(rel_path):
    rel_str = str(rel_path.parent)
    best_match, best_len = None, 0
    for folder, defaults in FOLDER_DEFAULTS.items():
        if rel_str.startswith(folder) and len(folder) > best_len:
            best_match, best_len = defaults, len(folder)
    return best_match
 
processed = 0
for md_file in sorted(VAULT.rglob("*.md")):
    rel = md_file.relative_to(VAULT)
    if any(skip in rel.parts for skip in SKIP_FOLDERS):
        continue
    content = md_file.read_text(encoding="utf-8")
    if has_frontmatter(content):
        continue
    defaults = get_defaults(rel)
    if not defaults:
        continue
    created = datetime.fromtimestamp(os.path.getctime(md_file)).strftime("%Y-%m-%d")
    updated = datetime.fromtimestamp(os.path.getmtime(md_file)).strftime("%Y-%m-%d")
    lines = ["---", f"type: {defaults['type']}", f"domain: {defaults['domain']}"]
    if "project" in defaults:
        lines.append(f"project: {defaults['project']}")
    lines += ["status: active", "tags: []", f"created: {created}", f"updated: {updated}", "---", ""]
    md_file.write_text("\n".join(lines) + content, encoding="utf-8")
    print(f"  ✅ {rel}")
    processed += 1
 
print(f"\nFrontmatter added to {processed} files")

Configuring Templater

Templater's best feature is folder templates — create a new note in a specific folder and the right template fires automatically. No extra steps, no menu navigation.

Shell
cat > /path/to/my-vault/.obsidian/plugins/templater-obsidian/data.json << 'EOF'
{
  "templates_folder": "_Templates",
  "trigger_on_file_creation": true,
  "auto_jump_to_cursor": true,
  "enable_folder_templates": true,
  "folder_templates": [
    { "folder": "00-Inbox",                          "template": "_Templates/Inbox-Capture.md" },
    { "folder": "10-Projects/my-app/Specs",          "template": "_Templates/Spec-Note.md" },
    { "folder": "10-Projects/my-app/Decisions",      "template": "_Templates/Decision-ADR.md" },
    { "folder": "10-Projects/my-app/Sprints",        "template": "_Templates/Sprint-Note.md" },
    { "folder": "20-Areas/Homelab",                  "template": "_Templates/Homelab-Note.md" },
    { "folder": "20-Areas/Learning",                 "template": "_Templates/Learning-Note.md" }
  ],
  "ignore_folders_on_creation": [
    { "folder": "_Templates" },
    { "folder": "_attachments" },
    { "folder": "40-Archive" },
    { "folder": "00-Home" }
  ]
}
EOF

After applying the config, assign hotkeys in Settings → Hotkeys → search "Templater":

Hotkey Template When to use
Alt+I Inbox Capture Quick capture — anything, anywhere
Alt+S Spec Note New feature or requirement
Alt+A Decision ADR Recording a significant decision
Alt+H Homelab Note New infrastructure documentation
Alt+L Learning Note Book, course, or research notes

Here is the Decision ADR template as an example — it shows how Templater's dynamic syntax works:

Markdown
---
type: decision
domain: project
project: <% tp.system.prompt("Project name") %>
status: <% tp.system.suggester(["proposed", "accepted", "rejected"], ["proposed", "accepted", "rejected"]) %>
tags: [decision, architecture]
---
 
# ADR — <% tp.file.title %>
 
**Date:** <% tp.date.now("YYYY-MM-DD") %>
**Status:** Proposed
 
---
 
## Context
 
> Why is this decision being made? What is the background?
 
<% tp.file.cursor() %>
 
---
 
## Decision
 
> What was decided?
 
---
 
## Reasoning
 
> Why this option and not another?
 
---
 
## Alternatives Considered
 
| Option | Why rejected |
| ------ | ------------ |
|        |              |
 
---
 
## Consequences
 
> What changes as a result? What follow-up decisions does this create?
 
---
 
## Revisit
 
**Date:** <% tp.date.now("YYYY-MM-DD", 90) %>

When you press Alt+A, a dialog prompts for the project name, a dropdown lets you pick the status, today's date fills in, and the revisit date is set to +90 days automatically. Your cursor lands at the Context section ready to type.

Building the Dataview dashboard

Dataview turns your vault into a queryable database. The Home.md dashboard shows live data — open tasks, inbox items, recent notes, active projects — updating automatically without any manual work.

Markdown
# 🧠 My Second Brain
 
## 🗺 Navigation
 
| | | |
|---|---|---|
| [[Projects-MOC\|🚀 Projects]] | [[../20-Areas/Homelab\|🖥 Homelab]] | [[../30-Resources\|📚 Resources]] |
 
---
 
## 📥 Inbox — Process These
 
```dataview
TABLE file.ctime AS "Captured"
FROM "00-Inbox"
WHERE file.name != "README"
SORT file.ctime ASC
```
 
---
 
## 🚀 Active Projects
 
```dataview
TABLE WITHOUT ID
  file.link AS "Project",
  status AS "Status",
  file.mtime AS "Last Updated"
FROM "10-Projects"
WHERE type = "spec" AND status = "active"
SORT file.mtime DESC
```
 
---
 
## 🏗 Recent Decisions
 
```dataview
TABLE WITHOUT ID
  file.link AS "Decision",
  project AS "Project",
  status AS "Status",
  file.mtime AS "Date"
FROM "10-Projects"
WHERE type = "decision"
SORT file.mtime DESC
LIMIT 8
```
 
---
 
## 📝 Modified Today
 
```dataview
TABLE WITHOUT ID
  file.link AS "Note",
  domain AS "Domain",
  type AS "Type"
FROM "10-Projects" OR "20-Areas" OR "00-Inbox"
WHERE file.mtime >= date(today)
SORT file.mtime DESC
```
 
---
 
## 🖥 Homelab Docs
 
```dataview
TABLE WITHOUT ID
  file.link AS "Document",
  status AS "Status",
  file.mtime AS "Updated"
FROM "20-Areas/Homelab"
SORT file.mtime DESC
LIMIT 6
```

Part 4 — Local AI with Ollama

Hardware requirements and model selection

Your GPU VRAM is the hard constraint. A model must fit in VRAM to run at GPU speed — spilling into RAM is 5-10x slower and causes Obsidian plugin timeouts.

Model VRAM needed Good for
gemma3:4b ~3.5GB Chat, Q&A, summaries — the daily driver
llama3.2:3b ~2.2GB Lightweight, fast responses
nomic-embed-text ~300MB (CPU) Embeddings for semantic search
llama3.1:8b ~5.5GB Exceeds 8GB with overhead — avoid on small GPUs

The lesson: a smaller model that fits in VRAM beats a larger model that spills into RAM. gemma3:4b answers in 5-10 seconds on an 8GB GPU. llama3.1:8b on the same card times out.

Setting up Ollama on Ubuntu

For the complete Ollama installation guide including GPU drivers and Open WebUI, see Local AI Powerhouse — Setting Up Ollama and Open WebUI on Ubuntu Server.

Once Ollama is running, pull the models:

Shell
ollama pull gemma3:4b
ollama pull nomic-embed-text
 
# Verify both loaded
curl -s http://localhost:11434/api/tags | python3 -m json.tool | grep '"name"'

Verify Ollama is reachable over Tailscale

From any laptop:

Shell
curl -s http://YOUR_HOMELAB_TAILSCALE_IP:11434/api/tags \
  | python3 -m json.tool | head -5
# Should return your model list — no additional config needed

Configuring Smart Composer

Install via Settings → Community Plugins → Browse → Smart Composer.

Configure it to use your local Ollama:

Python
import json
from pathlib import Path
 
config_path = Path("/path/to/my-vault/.obsidian/plugins/smart-composer/data.json")
 
with open(config_path) as f:
    config = json.load(f)
 
# Add Ollama models to the chat models list
ollama_models = [
    {"providerType": "ollama", "providerId": "ollama",
     "id": "ollama/gemma3:4b",   "model": "gemma3:4b"},
    {"providerType": "ollama", "providerId": "ollama",
     "id": "ollama/llama3.2:3b", "model": "llama3.2:3b"},
]
existing_ids = [m["id"] for m in config["chatModels"]]
for model in reversed(ollama_models):
    if model["id"] not in existing_ids:
        config["chatModels"].insert(0, model)
 
# Set Ollama as the active provider
config["chatModelId"]      = "ollama/gemma3:4b"
config["applyModelId"]     = "ollama/gemma3:4b"
config["embeddingModelId"] = "ollama/nomic-embed-text"
 
# Point to your homelab Ollama instance
for provider in config["providers"]:
    if provider["type"] == "ollama":
        provider["baseUrl"] = "http://YOUR_HOMELAB_TAILSCALE_IP:11434"
 
# System prompt — the single biggest lever for answer quality
config["systemPrompt"] = """You are a personal AI assistant embedded in an Obsidian vault.
 
VAULT STRUCTURE:
- 00-Inbox/ — unprocessed quick captures
- 10-Projects/ — active side projects with specs, decisions, and sprint notes
- 20-Areas/Homelab/ — server and infrastructure documentation
- 20-Areas/Learning/ — book notes, courses, research
- 30-Resources/ — code snippets, dev references, prompt templates
 
RULES:
- Be concise and direct — no filler phrases
- Always cite the source note when referencing specific information
- Never invent information not found in the notes
- For project questions → check the relevant project folder under 10-Projects/
- For infrastructure questions → check 20-Areas/Homelab/
- Format answers with bullet points for summaries, checkboxes for task lists"""
 
# Exclude non-content folders from RAG retrieval
config["ragOptions"] = {
    "chunkSize": 1000,
    "thresholdTokens": 8192,
    "minSimilarity": 0.2,
    "limit": 10,
    "excludePatterns": [
        "_attachments/**",
        "_Templates/**",
        "40-Archive/**",
        ".obsidian/**"
    ]
}
 
with open(config_path, "w") as f:
    json.dump(config, f, indent=2)
 
print(f"✅ Chat model:      {config['chatModelId']}")
print(f"✅ Embedding model: {config['embeddingModelId']}")

Restart Obsidian after running this script.

Using Smart Composer effectively

Smart Composer reads the currently open note as context by default. Open the relevant note first, then ask — the answers are grounded in your actual content.

Text
# With a Sprint note open
"What was completed in this sprint?"
→ Reads the note and summarises accurately
 
# With a project spec open
"What are the open questions in this spec?"
→ Finds the open questions section and lists them
 
# Adding context with @
@ my-app/Decisions/auth-decision.md
"What authentication approach did we decide on and why?"
→ Reads the ADR and explains the decision with reasoning

The system prompt is the single biggest lever for answer quality with a local model. A prompt that describes your vault structure and specifies how to format answers dramatically reduces hallucinations.

Monitor GPU load

While a response is generating:

Shell
# Run on homelab
watch -n2 'nvidia-smi --query-gpu=utilization.gpu,memory.used,memory.total \
  --format=csv,noheader && \
  curl -s http://localhost:11434/api/ps 2>/dev/null \
  | python3 -m json.tool 2>/dev/null | grep "name"'

If GPU utilization is near 0% while generating, the model is running from RAM — switch to a smaller model.


Part 5 — Backup Strategy

Three layers of protection

Text
Layer 1 (seconds):  Syncthing — all devices always in sync
Layer 2 (daily):    Git commit on homelab at 2:00 AM → push to GitHub
Layer 3 (manual):   Intentional snapshots for meaningful milestones

Each layer covers a different failure mode. Syncthing handles device failure. Daily git handles accidental deletion — git checkout -- filename.md restores any note. GitHub handles homelab failure — git clone restores the full vault to any device.

Create .gitignore

Before any commits, exclude files that should never go into version control:

Shell
cat > /path/to/my-vault/.gitignore << 'EOF'
# Obsidian device-specific state
.obsidian/workspace
.obsidian/workspace.json
.obsidian/workspace-mobile.json
.obsidian/cache
.obsidian/.cache
 
# Syncthing conflict and internal files
*.sync-conflict-*
.stfolder/
.stignore
 
# Smart Composer AI indexes (large binary files, regenerate automatically)
.smtcmp_vector_db.tar.gz
.smtcmp_json_db/
.smart-env/
 
# System files
.DS_Store
Thumbs.db
desktop.ini
*~
*.tmp
*.swp
EOF

Clean commit after restructuring

After any major restructure, clear the git index and recommit everything fresh so .gitignore takes effect on all files:

Shell
cd /path/to/my-vault
 
git rm -r --cached . --quiet
git add .
git commit -m "restructure: migrate to PARA layout + AI stack
 
- Reorganised to numbered folder structure (00/10/20/30/40)
- Added _Templates/ with folder-triggered Templater templates
- Configured Linter, Dataview, Smart Composer (Ollama)
- Backfilled frontmatter on all existing notes
- Disabled obsidian-git auto-commit — Syncthing handles sync
- Added .gitignore for AI indexes and device-specific files"
 
git push origin main

Automated nightly backup via systemd

Commits any changes at 2

AM and pushes to GitHub. Skips silently if nothing changed.

Shell
# Create the backup script
sudo tee /usr/local/bin/vault-backup.sh << 'SCRIPT'
#!/usr/bin/env bash
VAULT="/home/your_username/my-vault"
LOG="/var/log/vault-backup.log"
DATE=$(date '+%Y-%m-%d %H:%M:%S')
STAMP=$(date '+%Y-%m-%d')
 
cd "$VAULT" || exit 1
 
if git diff --quiet && git diff --cached --quiet && \
   [ -z "$(git ls-files --others --exclude-standard)" ]; then
    echo "[$DATE] No changes — skipping" >> "$LOG"
    exit 0
fi
 
git add -A
CHANGED=$(git diff --cached --stat | tail -1)
git commit -m "nightly: $STAMP — $CHANGED" >> "$LOG" 2>&1
 
if git push origin main >> "$LOG" 2>&1; then
    echo "[$DATE] ✅ Backup complete — $CHANGED" >> "$LOG"
else
    echo "[$DATE] ❌ Push failed — check SSH key" >> "$LOG"
    exit 1
fi
SCRIPT
 
sudo chmod +x /usr/local/bin/vault-backup.sh
sudo touch /var/log/vault-backup.log
sudo chown your_username:your_username /var/log/vault-backup.log

Create the systemd service and timer:

Shell
sudo tee /etc/systemd/system/vault-backup.service << 'EOF'
[Unit]
Description=Nightly vault backup to GitHub
After=network-online.target
Wants=network-online.target
 
[Service]
Type=oneshot
User=your_username
ExecStart=/usr/local/bin/vault-backup.sh
EOF
 
sudo tee /etc/systemd/system/vault-backup.timer << 'EOF'
[Unit]
Description=Nightly vault backup timer
Requires=vault-backup.service
 
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
RandomizedDelaySec=300
 
[Install]
WantedBy=timers.target
EOF
 
sudo systemctl daemon-reload
sudo systemctl enable vault-backup.timer
sudo systemctl start vault-backup.timer

Verify everything is working:

Shell
systemctl list-timers vault-backup.timer --no-pager
# Next run should appear in the output
 
# Test the script immediately
bash /usr/local/bin/vault-backup.sh
cat /var/log/vault-backup.log
# [2026-06-30 01:05:xx] No changes — skipping  ← correct

The Result

After building all of this, the day-to-day feels completely different.

Opening ObsidianHome.md shows what's in the Inbox, recent project activity, and any notes modified today. Navigation links jump to any area in one click.

Starting on something new — navigate to the right folder, press Ctrl+N. The template fires. Frontmatter is pre-filled, today's date is set, the cursor is where I need to type. I'm writing in under 3 seconds.

A thought comes up mid-flowAlt+I anywhere. Type the thought. It lands in Inbox. I'll process it later.

Need to find somethingCtrl+Shift+F for full-text search. For context-aware questions, open the relevant note, open Smart Composer, and ask. The answer comes back in 5-10 seconds from actual notes — running entirely on hardware I own, no data leaving my network.

End of the weekCtrl+P → Obsidian Git: Commit all changes. Write a meaningful commit message. Push. Done.

Every night at 2am, the homelab quietly commits whatever changed and pushes to GitHub:

Text
[2026-06-30 02:04:51] ✅ Backup complete — 4 files changed, 87 insertions(+)

The vault has 70+ notes, all with consistent frontmatter, all synced across three devices in real time — and a local AI that answers questions about any of them without a single byte leaving my infrastructure.


Troubleshooting

Syncthing shows a device as Disconnected

Shell
tailscale status
tailscale ping YOUR_HOMELAB_TAILSCALE_IP
 
# Restart Syncthing on homelab
sudo systemctl restart syncthing@your_username.service
sudo journalctl -u syncthing@your_username -n 30 --no-pager

Smart Composer times out or responds slowly

Check whether the model fits in VRAM:

Shell
curl -s http://localhost:11434/api/ps \
  | python3 -m json.tool | grep -E "name|size_vram"

If size_vram exceeds your GPU's capacity, switch to gemma3:4b or llama3.2:3b.

New notes not getting templates

  • Navigate into the folder in the sidebar before pressing Ctrl+N
  • Confirm trigger_on_file_creation: true in Templater settings
  • Folder paths in folder_templates are case-sensitive — verify exact match

Nightly backup failed

Shell
tail -20 /var/log/vault-backup.log
ssh -T git@github.com  # verify SSH key works
bash /usr/local/bin/vault-backup.sh  # run manually to see errors

Conflict file appeared

A .sync-conflict- file means you edited the same note on two offline devices. Open both files, merge the content you want to keep into the original, delete the conflict file.


What's Next

This post covers the foundation: sync, structure, plugins, local AI, and backup. The next layer is MCP (Model Context Protocol) — exposing the vault as a live API so Claude Code and GitHub Copilot agents can read your architecture decisions and project specs directly while you're coding. When your agent knows what you've already decided and why, its suggestions are grounded in your actual context rather than generic patterns.

That guide is coming next.

Key takeaways

  • Git auto-commit is the wrong tool for vault sync. Syncthing with a homelab hub is faster, cleaner, and leaves your git history meaningful. A note saved on your laptop at the office reaches your home server in under 10 seconds over Tailscale — without a single commit message.
  • Structure without friction. PARA with folder-triggered Templater templates means you never write frontmatter manually. Navigate to a folder, press Ctrl+N, and a fully structured note appears with today's date, the right metadata, and your cursor in the right place.
  • Local AI is viable for personal knowledge management. A small model on a consumer GPU answers questions about your vault in under 10 seconds — reading your actual notes, tasks, and project specs — without sending data anywhere.

// FURTHER EXPLORATION