I built a second brain in Go - Khayal
Apr 8, 2026
I have a problem with note-taking apps.
Not a preference problem. A trust problem.
Every tool I tried either lives on someone else's server, requires an account, or stores my thoughts in a format I do not fully own. Notion is great until Notion decides to change pricing. Obsidian is local but has no capture layer. Mem.ai is clever but completely cloud. And all of them want to be the center of your workflow.
Why not just use Obsidian with a plugin?
Because Obsidian has no capture layer. No CLI. No local LLM processing. No semantic search.
I did not want a center. I wanted a pipe.
So I built Khayal -- a local-first second brain that runs on your machine, processes with your own LLM, and stores everything as plain markdown. No accounts. No cloud. No telemetry. We genuinely do not know if anyone is using it.
خيال - Arabic for imagination, vision, the mind's eye.
Two binaries, not one
This was the first decision and it shaped everything else.
khayal is the server. It owns the database, the worker queue, the Ollama
calls, and the embedded PWA. It is the thing you start once and forget about.
kl is the client. It is a thin HTTP wrapper. It knows one thing: where the
server is and what the token is. Everything else is the server's problem.
Why not just one binary? Simpler to distribute.
Yeah, and also couples everything together. You cannot update the client without restarting the server. You cannot run the server on one machine and the client on another. The mental model gets muddy fast.
Two binaries is the right call. The added distribution complexity is worth it. This is the hard way and I knew the tradeoffs going in.
The CGo decision
Go's SQLite story is annoying.
The standard driver is mattn/go-sqlite3. It works great. It also requires
CGo, which means cross-compilation becomes painful, goreleaser needs a C
toolchain, and your Docker builds get complicated.
I did not want any of that. Khayal should be go build and done. So I used
modernc.org/sqlite -- a pure Go SQLite port transpiled from the original C.
No CGo. No toolchain requirements. goreleaser just works.
The tradeoff is sqlite-vec. The vector extension for SQLite is a C library and it does not play nicely with modernc. So right now Khayal does not have native vector search -- it has FTS5 keyword search and a cosine similarity fallback computed in Go. It works. It is not as fast as a proper vec index. I am still working through this.
If you have solved the sqlite-vec + modernc tension I genuinely want to know.
The options I see are a separate vec.db handled by a CGo process, or
accepting the pure Go cosine fallback forever. Neither is great.
This is a real open problem and not knowing it upfront would have been a major skill issue.
The search stack
Hybrid search with RRF -- Reciprocal Rank Fusion.
FTS5 handles keyword matching with porter stemming. The embedding model
(nomic-embed-text via Ollama) handles semantic similarity. Both produce a
ranked list. RRF merges them by taking the reciprocal of each result's rank
and summing across both lists.
The result is that kl search "observer effect" finds both exact matches and
semantically related notes -- depending on what is in your vault.
FTS5 configuration is where I lost the most time. It is very easy to misconfigure silently. Missing porter stemming, wrong column indexing, UNINDEXED columns that should be indexed -- none of these throw errors, they just quietly degrade your search quality. I found bugs in my own FTS5 setup weeks after I thought it was done.
Search quality is the core feature. Do not defer FTS5 configuration like I did. It is not infrastructure, it is the product.
The PWA is inside the binary
The PWA is embedded in the khayal binary via embed.FS.
One binary, one port, the web interface just works at http://127.0.0.1:1133.
No separate static file server. No deployment step. No nginx config. Just
start the server and open the browser.
The PWA has an offline capture queue backed by IndexedDB. If the server is down, captures queue locally and sync when it comes back. There is a live pipeline visualization that shows notes moving through the processing stages in realtime.
Getting iOS Safari right was the most tedious part of the whole project.
svh instead of vh for viewport height. The html background color must
match the nav bar color or Liquid Glass tinting breaks. @media (display-mode: standalone) needs to override certain layout calculations or
you get a gap at the bottom in PWA mode. None of this is documented in one
place. You just discover it by breaking things.
Your vault is just a folder
~/Documents/brain/
├── 2026-04-07/
│ └── observer-effect.md
├── 2026-04-06/
│ ├── react-patterns.md
│ └── system-design.md
└── .khayal/
Obsidian can open it directly. Git can version it. Any editor can read it. Khayal is a capture and retrieval layer -- it does not own your data, it just indexes it.
So Khayal replaces Obsidian?
No. They are complementary. Khayal is the pipe, Obsidian is the editor. Use both or neither.
What I would do differently
The worker queue overflow is a real failure mode I underestimated. Under load, if Ollama is slow and captures are coming in fast, the job channel fills up. I have a fix for v1.1 but it should have been in v1.
I would also start FTS5 configuration on day one. It feels like infrastructure so you defer it. Do not do that.
Try it
brew install rawnaqs/tap/khayal
khayal init
khayal start
kl "your first thought"
kl search "thought"
- GitHub: https://github.com/rawnaqs/khayal
- Site: https://khayal.rawnaqs.io
If you are working on anything local-first or have thoughts on the sqlite-vec problem, open an issue or reach out.
If you have feedback regarding this blog post, click on this
Thats it in this post, In case I don't see ya, Goodafternoon, Goodevening and Goodnight.