I like reading on my Kobo. Highlighting a passage and adding an annotation while reading feels natural enough. The annoying part starts when I want to get those annotations onto my computer and do something with them.

I use Obsidian for my notes, so that is where I wanted the passages to end up. Somewhere I could add my own thoughts, link to another note, and find them again without picking up the reader.

That became my weekend project: bring my Kobo highlights into Obsidian.

The initial idea was simple: export the highlights as Markdown.

Of course, once I started thinking about how I would actually use it, there were a few more requirements. What happens when I add another highlight? What if I change an annotation on the Kobo? What if I write something in Obsidian and the next export replaces it?

So this became a small application running on my NAS, a program on the Kobo, and an Obsidian plugin. A reasonable amount of software for moving some text between two devices (!)

In this post, I will go through how the pieces fit together and the decisions that mattered more than I expected.

Getting annotations off the reader

Even exporting a plain text file has some friction. A documented way to enable the device-side export, reported working on the Kobo Libra 2, is to connect it over USB and edit .kobo/Kobo/Kobo eReader.conf:

[FeatureSettings]
ExportHighlights=true

This exposes a per-book export option that saves the annotations as a .txt file on the reader. Then I still need to copy the file onto my computer and get its contents into my notes. The setting and export behavior can vary by model and firmware; it isn’t a requirement for every Kobo export method.

Kobo also has an official export through its website, including text and Markdown. That route is limited to books purchased from Kobo or obtained through Kobo Plus, and availability also depends on author approval.

So exporting is possible. What I wanted was a repeatable path into my vault, where new highlights could arrive without making me reorganize the same book every time.

What I wanted

I’ve written before about why I like keeping my notes as Markdown files. They are easy to move, search and edit. If I stop using a particular application, the files are still there.

I wanted the same thing for book highlights.

The workflow should let me:

  • Highlight a passage or write an annotation on the Kobo.
  • Send those changes to an application I host myself.
  • Browse the books and review passages from the browser.
  • Download them into my Obsidian vault.
  • Add personal notes without worrying about the next sync.

I also wanted to keep reading separate from maintaining the system. Highlighting a passage shouldn’t require deciding which note it belongs to or how I will use it later. I can do that when I come back to it.

The overall structure

There are three main parts:

flowchart LR K["Kobo<br/>Highlights + annotations"] -->|HTTPS upload| A["NAS app<br/>Library + reviews"] A -->|Plugin downloads| O["Obsidian<br/>Local Markdown notes"] classDef reader fill:#edf5ee,stroke:#9db6a2,color:#264d37 classDef app fill:#edf2fa,stroke:#9eafcb,color:#2d4b76 classDef vault fill:#f3eef9,stroke:#b6a4cf,color:#5a427b class K reader class A app class O vault

The Kobo sends the source data. The app keeps an imported library and provides a browser interface. The Obsidian plugin downloads the book data and creates the Markdown files locally.

flowchart TB subgraph KOBO["KOBO · CAPTURE"] direction TB SOURCE["Highlights + annotations<br/>Read-only database snapshot"] EPUB["EPUB metadata + covers<br/>Context snippets are opt-in"] PUSH["Native Go uploader<br/>NickelMenu sync"] SOURCE --> PUSH EPUB --> PUSH end subgraph APP["KOBO NOTES · ORGANIZE"] direction TB API["Go service over HTTPS<br/>Self-hosted service"] DATA[("SQLite library")] WEB["React / Vite web app<br/>Browse, annotate, review, organize"] EXPORT["Automatic Markdown export<br/>Server-side book library"] API <--> DATA WEB <-->|"Browser interface"| API DATA -->|"After imports and app edits"| EXPORT end subgraph MAC["OBSIDIAN · CONNECT IDEAS"] direction TB PLUGIN["Companion plugin<br/>Manual or periodic downloads"] VAULT["Local vault · 09-Books<br/>Overviews, covers, highlight notes"] PERSONAL["Personal notes + local overrides<br/>Reviews, tags, collections"] PLUGIN -->|"Safe Markdown updates"| VAULT PERSONAL <--> VAULT end PUSH -->|"1 · Secure snapshot upload"| API API -->|"2 · Plugin pulls books + covers"| PLUGIN classDef capture fill:#eaf3ec,stroke:#8dae96,color:#234c38,stroke-width:1.3px classDef service fill:#eaf0f9,stroke:#96acce,color:#254773,stroke-width:1.3px classDef notes fill:#f0ebf8,stroke:#b4a2cf,color:#584277,stroke-width:1.3px class SOURCE,EPUB,PUSH capture class API,DATA,WEB,EXPORT service class PLUGIN,VAULT,PERSONAL notes style KOBO fill:#f7faf7,stroke:#c8dacb,stroke-width:1px style APP fill:#f6f8fc,stroke:#ccd7e9,stroke-width:1px style MAC fill:#faf8fc,stroke:#d9cfe7,stroke-width:1px

The detailed architecture. The server export and the local Obsidian library are independent.

One detail here is that the plugin downloads data from the app’s API. It doesn’t copy a folder from the NAS.

The app can also generate a Markdown library on the server, but that export is independent. I don’t have to mount a network share or keep the vault on the NAS to use the plugin. My working vault stays on the Mac and remains available offline.

If I want a separate folder backup to the NAS, that can be its own sync task.

Getting the data out of the Kobo

The Kobo keeps its reading data in a SQLite database. The uploader creates a consistent snapshot and sends it to the service over HTTPS. The source database is read-only throughout the process.

I added a sync action to NickelMenu so I can start the upload from the reader itself.

The uploader is written in Go, and it can also include covers and chapter metadata from the EPUB files. Chapter names are useful when browsing a book later. I don’t want to look at an internal filename and guess which chapter it represents.

There is also an optional sync with passage context. More on that later.

sequenceDiagram actor Me participant Kobo participant App as NAS app participant Plugin as Obsidian plugin participant Vault as Local vault Me->>Kobo: Start NickelMenu sync Kobo->>Kobo: Create read-only database snapshot Kobo->>App: Upload snapshot and available book assets App->>App: Import using stable Kobo IDs App->>App: Update server Markdown export Note over App,Plugin: Separate download step Me->>Plugin: Download now (or enable periodic downloads) Plugin->>App: Request books and covers App-->>Plugin: Book data and available covers Plugin->>Vault: Merge source updates and keep personal writing

Starting a Kobo sync updates the app. Obsidian downloads those changes separately, manually or on its optional timer.

This integration reads from the Kobo. Reviews, tags and notes created in the app or Obsidian don’t get written back to the reader.

A small app on the NAS

The server is a Go service with a SQLite database and a React/Vite interface. It runs on my NAS and is accessible over HTTPS.

The app gives me another place to work with the highlights. I can search across books and passages, read Kobo annotations, add notes, mark passages as reviewed, and organize them with tags and collections.

I wanted this to work from the browser too. Opening Obsidian shouldn’t be a requirement just to revisit a passage.

Imports use the Kobo’s book and highlight IDs to update existing records. Sending the same book again should update its data rather than create another copy of it.

That sounds like a small implementation detail, but almost every other part depends on it. A title can change. An annotation can change. The application still needs to know that it is looking at the same highlight.

The files in Obsidian

The companion plugin gives me a library view inside Obsidian, with covers, search and filters for reading and review status.

The current plugin in my vault, showing 42 books and 803 saved passages. The sync report also shows an edited-source conflict; the affected note was left untouched.

Inside the vault, the library lives under 09-Books:

09-Books/
Book Title/
Overview.md
cover.jpg
Highlights/
01-001 - Short highlight title.md
01-002 - Another highlight.md

The book folder uses the book’s name. The IDs stay in the metadata where the integration needs them. I don’t need to see an identifier every time I open the sidebar.

Each book has an overview and each highlight gets its own note. This makes it easy to link a particular passage from another note instead of linking to a long document and hoping I find the right part later.

I initially included more information in the highlight notes: chapter progress, separate annotation headings and comments used by the exporter. Looking at the result, it felt too busy.

What I want to read is the passage and my note, if I wrote one.

So the visible content is simpler. Identity and overwrite protection live in frontmatter. The plugin can use the metadata without making the body look like an export report.

The files also work without the plugin. The dashboard and explorer are convenient ways to browse them, but the library is still ordinary Markdown.

The part I cared about most: keeping my own writing

Creating a Markdown file is easy. Updating it after someone has edited it is where things get interesting.

The integration needs to distinguish imported content from personal writing. The quotation and imported annotations belong to the source. My own notes belong to me.

When a new download arrives, it updates the source content while preserving my writing. If I edit generated content in a way that conflicts with the incoming version, the update reports a conflict and leaves that note untouched.

I would rather resolve a visible conflict than find out later that a sentence I wrote disappeared.

The same approach applies when a source highlight goes missing. Its local note is retained and flagged instead of being silently deleted. A highlight may have become the starting point for a longer thought by then. Its value isn’t limited to whether it still exists on the reader.

Review state needs a direction

Reviewing passages in both places introduced another question.

Suppose I mark a passage as reviewed in the app. That state should appear in Obsidian when I download it. But then I decide to mark it as not reviewed in Obsidian because I want to come back to it.

The next download shouldn’t mark it reviewed again.

The plugin handles this with a local override. It follows the app’s review state until I make an explicit local choice. After that, it preserves my choice, including marking something as not reviewed.

flowchart TB subgraph APP["01 · IN THE APP"] A["✓ Reviewed<br/>I have already processed this passage"] end A -->|Download| D{"Have I made a local<br/>review choice?"} subgraph FOLLOW["02 · FOLLOW THE APP"] B["✓ Reviewed in Obsidian<br/>Use the app's current state"] end subgraph LOCAL["03 · MY VAULT, MY CHOICE"] C["↻ Awaiting review in Obsidian<br/>I want to revisit this passage"] L["Local override is active<br/>Keep my explicit choice"] C --> L end D -->|"No · follow app"| B D -->|"Yes · keep local choice"| L B -->|"I mark it not reviewed"| C L -->|"Next download"| D L -->|"Use app review state"| R["Clear the local override"] R --> B classDef app fill:#edf2fa,stroke:#9eafcb,color:#2d4b76,stroke-width:1.5px classDef choice fill:#fff5dd,stroke:#c8ab60,color:#705719,stroke-width:1.5px classDef follow fill:#edf5ee,stroke:#9db6a2,color:#264d37,stroke-width:1.5px classDef local fill:#f3eef9,stroke:#b6a4cf,color:#5a427b,stroke-width:1.5px classDef reset fill:#f5f5f2,stroke:#b5bbb6,color:#4e5a52 class A app class D choice class B follow class C,L local class R reset style APP fill:#f7f9fd,stroke:#ccd7e9 style FOLLOW fill:#f7faf7,stroke:#c8dacb style LOCAL fill:#faf8fc,stroke:#d9cfe7

The app provides the default. A local choice takes priority on every later download, until I explicitly reset it.

If I want to follow the app again, I can clear the override with Use app review state.

Tags and collections follow a similar approach. Local changes survive downloads, and I can choose to use the app’s values again later.

This isn’t two-way synchronization. Obsidian doesn’t upload these changes to the app. For this version, having a clear direction and protecting local decisions is enough.

A little more context

Sometimes a highlighted sentence needs the paragraph around it to make sense.

The optional context-enabled sync extracts surrounding text from the EPUB and sends a small snippet with the passage. It doesn’t upload the whole book.

The app and plugin can display that context when it is available. Including it in the Markdown note is a separate option and starts disabled. I still want the default note to be small.

The book view for A Philosophy of Software Design by John Ousterhout, inside Obsidian. With Show context expanded, I can read the surrounding text and see the highlighted passage in place. The underlying highlight note is still one click away.

The example above shows context available in my actual library. It is useful for checking what a passage meant without turning every highlight note into a long excerpt. Other books and EPUB layouts still need their own checks; this one example doesn’t tell me how every book on the device behaves.

What I want to see next

The useful part of this project will be what happens after the export.

Can I find a passage when I remember the idea but forget the book? Can I connect something I read to a note I was already working on? Does reviewing a highlight lead to writing anything in my own words?

Those are harder questions than whether the sync completed successfully.

For now, the pieces are in place: capture on the Kobo, browse in the app, and keep the notes in my vault. I can improve the interface later. What I wanted first was a way to bring the material into the place where I already think and write, without losing my own work the next time I press sync.


Cover photo by Lala Azizli on Unsplash.