← All projects

PKMA

A private, local-first chatbot for Markdown second brains, journals, and personal Obsidian vaults.


0 KB

uploaded by design

The vault, index, vector store, and model provider all stay under the user's control.

1 binary

desktop distribution

A Tauri shell packages the app without Electron, Docker, or a separate Python service.

3 modes

agent depth

Fast, Normal, and Deep Research modes tune tool budgets, retrieval size, and graph traversal.

The PKMA desktop app answering a question from a local SecondBrain vault, with a live telemetry panel showing tokens used, throughput, and the selected model provider.

PKMA answering from a local vault, with the live telemetry panel tracking token usage, throughput, and the active model.

PKMA is a private, local-first chatbot for Markdown second brains. It is meant for the notes you do not want to upload anywhere: journals, private thoughts, drafts, research notes, and the rest of an Obsidian vault.

The app points at a folder of Markdown, builds a local SQLite index, talks to a local or user-selected model provider, and answers with citations to the notes it used. The point is not to make another generic document chatbot. The point is to make something safe enough to use next to a private journal.

What I built

PKMA ships as a desktop app with two main parts:

Rust core

A Tauri v2 backend starts a local Axum server on 127.0.0.1:8008. It owns vault indexing, SQLite access, provider calls, and the agent loop.

Next.js webview

A statically exported Next.js interface runs inside the desktop shell and talks to the Rust core over local HTTP and server-sent events.

Local retrieval

Each vault gets an isolated SQLite database with FTS5 keyword search and sqlite-vec semantic search, fused with reciprocal-rank fusion.

Bring-your-own model

Chat and embeddings work with Ollama, LM Studio, OpenRouter, llama.cpp, or any compatible OpenAI-style endpoint.

It looks like a normal chat app, but most of the work is in the fully local plumbing: parsing Markdown, keeping Obsidian structure intact, refreshing the index, and tying each answer back to a real note title.

How It Works

There is no hosted backend for the app. The desktop binary runs the UI and the local API, then stores each vault’s index on disk.

flowchart TB
    user["Obsidian user"]
    webview["Next.js webview"]
    api["Axum local API 127.0.0.1:8008"]
    agent["Rig agent loop"]
    tools["vault tools"]
    db["SQLite FTS5 + sqlite-vec"]
    linkGraph["wikilink graph with petgraph"]
    provider["Ollama / LM Studio / OpenAI-compatible provider"]
    vault["Markdown vault"]

    user --> webview
    webview -->|HTTP| api
    api -->|SSE| webview
    api --> agent
    agent --> tools
    tools --> db
    tools --> linkGraph
    db --> vault
    linkGraph --> vault
    agent --> provider

That keeps the privacy story simple. With a local provider, PKMA can run without the network. With a hosted provider, the vault and index still stay off remote storage.

Finding Notes

The agent has to work with notes the way people actually write them: wikilinks, frontmatter, tags, aliases, daily notes, half-finished drafts, and personal naming habits.

The retrieval layer combines several signals:

  • FTS5 keyword search for exact terms, note names, and phrases.
  • Semantic vector search for ideas written in different words.
  • Reciprocal-rank fusion to merge keyword and semantic results.
  • Wikilink graph traversal so connected notes can surface even when embeddings miss them.
  • Citation resolution so note pills show whether a title matched exactly, through FTS, through a fuzzy fallback, or not at all.

Indexing and Streaming

Indexing is incremental. A watcher notices file changes, note bodies are hashed with blake3, unchanged files are skipped, and files that fail parsing are quarantined so they do not keep cycling as “changed.” Switching vaults can cancel work in progress, which matters when one large vault is still embedding and another one opens.

Chat streams over SSE. Tokens, thinking steps, tool calls, and final messages are separate events, so the UI can feel live without adding a websocket service.

sequenceDiagram
    participant UI as Next.js webview
    participant API as Axum API
    participant Agent as Agent loop
    participant Tools as Vault tools
    participant DB as SQLite index
    participant LLM as Model provider

    UI->>API: POST /chat
    API->>Agent: start run
    Agent->>Tools: vault_search
    Tools->>DB: FTS + vector search
    DB-->>Tools: ranked notes
    Tools-->>Agent: cited context
    Agent->>LLM: stream completion
    LLM-->>API: tokens
    API-->>UI: SSE events

What I learned

The hard part was not putting a chat box next to a model. It was making the local system trustworthy enough for private notes:

  • SQLite needs clear read/write boundaries when the UI, indexer, and chat loop are all active.
  • Obsidian vaults are messy in useful ways, so the parser and indexer need to tolerate imperfect Markdown.
  • Citations are a product feature, not decoration; users need to know which note an answer came from.
  • Local-first software still needs good cancellation, progress, and recovery states because large vaults make every long-running task visible.

PKMA is still evolving, but it is already close to what I wanted: a private research partner for my own notes, with local data and model choice built in.