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.
┌─────────────────────────────────────┐
│ 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 syncWhat 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.
- You save the note on your work laptop. Syncthing detects the new file immediately.
- 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.
- 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.
- 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
# 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 syncthingRun Syncthing as your normal user (not root) via systemd:
# 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:
mkdir -p /home/your_username/my-vault
syncthing --device-id
# Output: MFZWI3D-BONSGYC-YLTMRWW-... ← save thisAccess the web UI from your laptop via SSH tunnel (the homelab is headless):
# Run this on your laptop
ssh -L 8384:localhost:8384 your_username@192.168.1.100
# Then open: http://localhost:8384Set a strong password immediately — Actions → Settings → GUI tab → Authentication.
Installing Syncthing on Ubuntu laptops
Identical process on each laptop:
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.serviceOpen http://localhost:8384 and note the Device ID:
syncthing --device-idSetting 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:
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up
# Open the printed URL, log in with the same account on every deviceGet your homelab's Tailscale IP — you'll use this address everywhere:
tailscale ip -4
# Returns: 100.x.x.x ← save thisVerify from a laptop:
tailscale ping 100.x.x.x
# Expected: pong from homelab in XmsPairing devices in Syncthing
On the homelab Syncthing UI (via SSH tunnel):
- Click Add Remote Device → paste your laptop's Device ID → name it
laptop-work - Repeat for each device
- Click the vault folder → Edit → Sharing tab → check all devices
- 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:
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
EOFDisable 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:
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"
}
EOFVerify end-to-end sync
# 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.mdPart 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.
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 automaticallyThe 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:
Settings → Files & Links
→ Default location for new attachments
→ In the folder specified below → _attachmentsNew notes — so everything lands in Inbox by default:
Settings → Files & Links
→ Default location for new notes
→ In the folder specified below → 00-InboxPart 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
createdandupdateddates, 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+Ninside10-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.mddashboard: 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.
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"]
}
EOFThe frontmatter schema
Consistent frontmatter makes notes queryable by both Dataview and AI. Every note follows this schema:
---
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:
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.
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" }
]
}
EOFAfter 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:
---
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.
# 🧠 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:
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:
curl -s http://YOUR_HOMELAB_TAILSCALE_IP:11434/api/tags \
| python3 -m json.tool | head -5
# Should return your model list — no additional config neededConfiguring Smart Composer
Install via Settings → Community Plugins → Browse → Smart Composer.
Configure it to use your local Ollama:
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.
# 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 reasoningThe 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:
# 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
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 milestonesEach 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:
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
EOFClean commit after restructuring
After any major restructure, clear the git index and recommit everything fresh so .gitignore takes effect on all files:
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 mainAutomated nightly backup via systemd
Commits any changes at 2
AM and pushes to GitHub. Skips silently if nothing changed.# 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.logCreate the systemd service and timer:
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.timerVerify everything is working:
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 ← correctThe Result
After building all of this, the day-to-day feels completely different.
Opening Obsidian — Home.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-flow — Alt+I anywhere. Type the thought. It lands in Inbox. I'll process it later.
Need to find something — Ctrl+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 week — Ctrl+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:
[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
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-pagerSmart Composer times out or responds slowly
Check whether the model fits in VRAM:
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: truein Templater settings - Folder paths in
folder_templatesare case-sensitive — verify exact match
Nightly backup failed
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 errorsConflict 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.