I had ten years of notes in Standard Notes. Around 500 of them: recipes with version histories, client work going back to 2016, travel, a car build, taxes, people, journal fragments. I was paying for a service I was about to stop using, and the notes were going nowhere. Barely tagged. Zero links. One undifferentiated pile with a 6-27-2020-import tag sitting on 58 of them from some migration I did six years ago.
Meanwhile my Obsidian vault was already live and syncing, end to end encrypted, between my phone and my own server. It had maybe six notes in it.
This is the record of closing that gap. Export the decade, run the notes through a two-model AI classification pipeline, build the linking layer on top, then automate the whole thing so new notes file themselves. Total LLM cost: about fifteen cents.

TL;DR
- Exported Standard Notes decrypted, which preserves everything: titles, bodies, timestamps, tag-to-note maps, even nested tags.
- Built a converter to Markdown with YAML frontmatter, then ran a two-model pipeline over all 479 non-trashed notes. GLM-5.3-Flash classified, DeepSeek V4 Flash double-checked.
- The two models agreed on about 85% of placements. Disagreements and low-confidence calls got flagged instead of guessed. That shrank the pile I had to personally review from 410 notes to 29.
- Built the connective tissue afterward: Maps of Content, version chains between recipe iterations, people backlinks, live database views.
- A nightly cron job now files anything I drop into the inbox using the same pipeline. The inbox tends toward empty.
The sync was already there
Before the migration could matter, the notes had to live somewhere I trusted, and that part was its own project worth describing, because it is why none of this is locked into a vendor.

For syncing I use Self-hosted LiveSync, a community plugin for Obsidian with about 13,000 stars on GitHub. It is genuinely excellent, and the design is the interesting part: instead of syncing files, it syncs your vault into a CouchDB database, which is a document database built for exactly this problem. CouchDB has handled multi-device replication with conflict resolution since the mid-2000s, and it speaks plain HTTPS, so any device anywhere can reach it without a VPN.
And with end-to-end encryption turned on in the plugin, the server never sees a note in plaintext. The database on my server is a pile of encrypted blobs. If someone walked off with the server, they would get nothing useful. That is the part I could never get from a cloud notes app with a “trust us” privacy policy.
I run CouchDB 3.5 on my home Kubernetes cluster, which I wrote about in the mini data center post. The database sits on a persistent volume, and it reaches the internet through an outbound tunnel, so the house has zero open ports. A private hostname with a real certificate, reachable from anywhere. The phone and the desktop both talk to it through the Obsidian plugin, and the cluster hosts it for free since the hardware already exists.

The setup had a few sharp edges worth recording, because if you try this you will hit the same ones:
The first device defines everything. The encryption passphrase and connection settings get decided once, on the first device you configure. Every other device joins by receiving a Setup URI, an encrypted link that the first device generates from its command palette. If you lose the passphrase, you lose the data. There is no reset.
Two things in Obsidian are called “sync.” The app itself ships a red “uninitialized” sync icon that belongs to Obsidian’s built-in paid Sync service. It has nothing to do with the LiveSync plugin. I lost real time to that icon; it is a decoy. The plugin’s own status lives in its settings page and its status bar.
The sync method setting matters. The plugin defaults to syncing only on events, which quietly never fires on a new install. Setting the method to “LiveSync” or “Periodic and on events” is what makes notes actually upload. My first three test notes sat local-only until I found that.
Verify from the server, not the clients. The plugin’s status bars can lie while a replication is stuck. The ground truth is CouchDB itself: query the database’s document count and change history, and compare each device’s replication checkpoint. Both directions of my sync were only “working” after the server reported the test notes arriving and the sequence numbers advancing. Client-side smiles, server-side proof.
Once the server held 19 documents and both devices’ checkpoints had consumed every change, sync was verified in both directions, and the old test notes pulled down onto the desktop exactly as they existed on the phone. That was the green light for the migration.
The export
Standard Notes’ Decrypted JSON export is genuinely good. It is one file containing every item as JSON with creation dates, update dates, UUIDs, and a references array that maps tags to notes and tags to parent tags. What looks like a backup file is actually the cleanest structured source I could have asked for. The desktop app also writes per-note text files, but those are redundant. The main JSON has everything, including which notes used rich text editors versus plain markdown.
One honest flag: the export is decrypted plaintext of everything, including notes you forgot were sensitive. Mine contained a game account’s recovery codes on the first screenful. Stage it somewhere local, migrate, then remove it.
Why not just export and drag files over
Because the export is not the migration. A folder of 479 markdown files with no tags, no links, and no organization is just a pile with better formatting. The value of Obsidian specifically is the graph: backlinks, hub notes, queryable properties. None of that exists in a Standard Notes export.
The tags that did exist had drifted. bread::pastry and recipes::bread coexisted. arete and Work::Arete described the same company. A typo tag (outso) had quietly become load-bearing. And 73% of the notes had no tags at all.
So the plan split into layers. Get the content out faithfully. Classify it. Normalize the taxonomy. Then build the link structure that turns a pile into a graph.
flowchart LR
SN["Standard Notes
decrypted JSON export"] --> CV["converter
markdown + frontmatter"]
CV --> G["GLM-5.3-Flash
classify all 479"]
G --> D["DeepSeek V4 Flash
second opinion"]
D --> MG{"merge"}
MG -->|"agree or high conf"| F["filed into vault"]
MG -->|"disagree / unsure"| R["_inbox/ for human"]
F --> L["MOCs + version chains
people backlinks + Bases"]
R --> L
The classification pipeline
I ran a 20-note pilot first, both models on the same notes, and diffed the results. That pilot paid for itself twice.
First, it caught an infrastructure bug that would have looked like model stupidity. Both models kept “failing” on some notes, and the failures turned out to be my own token budget: I had capped output at 300 tokens, and these are reasoning models that think before they write. They were using up the budget on reasoning and producing nothing. Raising the cap fixed nine of the twenty “errors.”
Second, the pilot revealed the models’ different failure modes. DeepSeek was fast, about two seconds per note, and produced clean tags. But twice it violated the schema: once it invented a folder that was not in the allowed list, and once it silently omitted a required field. GLM was slower but never broke the schema, and its confidence was better calibrated. It said “medium” when it was genuinely unsure instead of pretending. GLM became the primary classifier and DeepSeek the second opinion.
The merge rules were the important part:
| Situation | Action |
|---|---|
| GLM high confidence, DeepSeek agrees | file it |
| Both agree independently | file it, highest trust |
| GLM unsure, DeepSeek confident | take DeepSeek’s folder |
| Both high confidence but different folders | file GLM’s, flag it for later review |
| Either model unsure | stays in the inbox, annotated |
That last row is the design decision I would keep. The failure mode of AI-assisted organization is confident misfiling: a note about guitar shopping landing in cooking because a keyword matcher saw the word “chili.” Once a note is wrong you never see it again, because it looks organized. Flagging instead of guessing meant my personal review pile was 29 notes instead of 410, and every one of those 29 was genuinely ambiguous.
A frozen vocabulary mattered too. The models could only choose folders from a fixed list and tags from the existing taxonomy, so they could not spawn 200 one-off tags, which is the classic failure of letting an AI organize anything. Where the taxonomy had a real gap, the models’ attempts to route around it made the gap visible. I had real soda-making content and no soda folder; the models tried to shoehorn it, and adding cooking/soda to the vocabulary fixed every one of those notes at once.
What the vault got that a folder dump would not

Maps of Content. Each area of the vault has a moc.md that is nothing but links: every note in the area, grouped by subfolder. Projects went two-tier, with a top hub linking sixteen client MOCs. In the graph view the MOCs are the hubs everything hangs off.

Version chains. Baking notes iterate. The same Pullman loaf exists as V1, V2, and V3 with different hydration and flour. The pipeline detected version-suffixed notes and linked each to its earlier and later siblings. Ten years of “what did I change between batch 2 and batch 3” is now one click.
People backlinks. Every note in people/ became a matcher, and a scan over all note bodies found mentions and appended a people section with links. Old work logs now point at person notes, which hold the contact info and the context.
Bases. Obsidian’s built-in Bases feature turned the frontmatter into live tables with no extra plugins: a recipe index sortable by tag and date, a technique view, a journal timeline, and my favorite, an import-audit view of every note the models flagged. The review queue became a filterable table instead of a memory exercise.
Searching the graph works the way I always wanted a notes app to work. Type “sourd” and the graph collapses to the sourdough cluster:

The part that runs without me
Filing 479 historical notes was a project. Filing the next note should not be. So the last piece is a nightly cron on the server:
flowchart LR
P["capture on phone
into _inbox/"] -->|"livesync"| S["server, nightly 03:33"]
S --> A["auto-taxy script
classify + enrich"]
A -->|"high confidence"| M["filed, tagged, linked,
MOC updated"]
A -->|"unsure"| I["stays in _inbox/
with suggested folder"]
M --> B["weekly tarball
to the NAS"]
Anything dropped into the vault’s inbox gets the same treatment the historical notes got: classified, tagged with normalized tags, enriched if it is a recipe, moved into place, and the MOCs regenerate around it. The principle is the same flag-don’t-guess: the agent moves what it is sure about and annotates the rest. A note captured on the phone during the day is filed by the next morning.
The backup is separate from the sync on purpose. LiveSync handles replication between devices. The weekly tarball to the NAS handles the case where I delete the wrong thing on every device at once. Append-only, dated, small.
What it cost
| Item | Cost |
|---|---|
| Classification, 479 notes across 2 models | under $0.15 |
| 20-note pilot, both models | about a penny |
| Recipe enrichment pass, 165 notes | about a penny |
| Nightly auto-taxy, ongoing | fractions of a cent per note |
| Sync server | free, on hardware I already own |
| Standard Notes subscription | retired |
The token math is boring because the notes are short; the median one is under 400 bytes. The real cost was attention: designing the taxonomy, reading the pilot diff, walking the inbox. The models did the labor. The human decisions took about 30 minutes total, and that ratio is exactly what makes this kind of migration feasible.
For a business reader, that is the takeaway. The monthly line items in your notes app, your sync service, and your “second brain” subscription are all replaceable with free software on hardware you already own, plus pennies of AI tokens and one afternoon of decisions.
What I would do differently
- Pilot first, always. The 20-note run caught a token-budget bug and chose my primary model. Skipping it would have meant discovering both on the full run.
- The export format matters more than the tool. The JSON export with its reference maps made tag conversion lossless. A plain text export would have lost the tag relationships entirely.
- Frozen vocabularies beat clever prompts. Every taxonomy problem after the pilot was a vocabulary gap, fixable in one line, not a prompt engineering problem.
- Flag-don’t-guess is what made it trustworthy. 85% agreement sounds worse than full automation until you notice that the 15% is exactly the pile where automation is wrong.
- Trust the database, not the status bar. For sync, for backups, for anything that claims it happened. The server’s change log is the only witness that counts.
The vault is 464 notes now, and the number that are unorganized trends toward zero instead of infinity. That is the part I would have wanted future-me to know was possible.