hey 👋
Lore is an offline-first personal knowledge base with global-hotkey quick capture. Press ⌥Space anywhere, drop in a link, note, task or code snippet, and Lore files it into a folder of Markdown files you can browse, search, ask questions about — and commit.
I built it because every tool I tried made me choose between a nice reading surface and owning my data. Lore is:
- a capture bar that stays out of the way — ⌥Space, type, enter, gone
- a knowledge base — sidebar + list + detail, with list, card and table views
- a folder of
.mdfiles — not a database with an export button
Your library is a directory of Markdown files with YAML frontmatter. The files
are the data. Point Lore at a git repo, edit a note in Obsidian or vim,
git pull on another machine — the app follows along without a restart.
The screenshots in this README are rendered straight from the Claude Design sources (
Lore.dc.html,Lore Knowledge Base Views.dc.html), which the UI is recreated from pixel-for-pixel.
- 🗂️ The filesystem is the database. Folders are collections, files are items. Move a file in Finder and it is refiled in Lore.
- ✍️ Never lose what you typed. Unknown frontmatter keys survive a
round-trip; a
[[wikilink]]to a note that does not exist yet is preserved byte-for-byte and heals itself when the target appears. - 🔌 Offline first, network never required. No account, no sync service, no telemetry. AI is a pluggable provider and ships as a deterministic mock.
- 🔒 The renderer is not trusted. The webview gets no shell permission; the data engine is spawned by Rust and reached over a loopback port with a bearer token passed by environment variable, never argv.
- ⚡ Capture in under a second. A global shortcut, a frameless panel, and two ways in — a command bar or a composer.
- ♻️ Derived state is disposable. Delete
.lore/index.dband it rebuilds from the files. It is never migrated; a schema change throws it away. - 🧭 No surprise renames. Retitling an item does not rename its file, because that would rewrite every inbound link and churn your git history.
- 🗑️ Deletes move to trash, they do not unlink.
- 🙈 Lore never runs git. The vault is a folder. Committing it is up to you.
The capture window is frameless, always on top, and offers both directions.
Direction A — Command bar. Type anything; AI detects what it is, titles it, tags it and files it.
Direction B — Composer. Pick a type up front, add structure, then save.
⌥Space toggles between them, and files can be dragged straight onto the panel.
The list pane renders three ways. Table puts collection, tags, type and age in columns — the view for triaging a backlog rather than reading it.
Cards is the browsing view; items with a banner URL show a hashed placeholder while the preview loads.
Every surface is themed from one token set, so dark mode is a repaint, not a second stylesheet.
Lore is not packaged yet. Build it from source:
pnpm install
pnpm tauri dev # launches the desktop app (first run compiles Rust)Other scripts:
pnpm build # type-check + build the web bundles
pnpm test # vitest (renderer) + bun test (data engine)
pnpm dev:sidecar # run the data engine alone, on a fixed dev port
pnpm build:sidecar # compile the engine binary Tauri bundles
pnpm tauri build # produce a distributable .app/.dmgThree Lores can run at once — the one you installed, the one you are working on,
and one an agent is changing under you — so each is a separate app that says
which it is. LORE_MODE picks one; lore.modes.json is the table they all read.
pnpm tauri dev # Lore Dev, amber
LORE_MODE=agent pnpm tauri dev # Lore Agent, violet
pnpm tauri build # Lore, the shipping identity| Mode | App | Vault | Vite | Engine | Quick capture |
|---|---|---|---|---|---|
prod |
Lore |
…/com.lore.app/Vault |
1420 | random | ⌥Space |
dev |
Lore Dev |
…/com.lore.app.dev/Vault |
1430 | 51799 | ⌥⇧Space |
agent |
Lore Agent |
…/com.lore.app.agent/Vault |
1440 | 51809 | ⌥⌃Space |
A labelled build tints its Dock and menu-bar icons, shows its label beside the
traffic lights and in the menu bar, and draws a hairline of its colour along the
top of every window. Its identifier is its own, so it opens its own vault and
keeps its own preferences — pnpm tauri dev will not touch the library the
installed app is using. Settings → About names the build and the vault it opened.
Two modes out of one checkout share a Cargo target directory, so switching
between them rebuilds the Rust side and running both at once blocks on the build
lock. Give each its own worktree — or set CARGO_TARGET_DIR — when you want two
running side by side.
Regenerate the tinted icon sets after changing a master or an accent:
node scripts/mode-icons.mjsFirst launch shows onboarding: sign in with Apple / Google / an email link, or start a local vault. Identity providers and mail delivery are stubs today, so start a local vault is the path that works end to end.
You get a global vault at ~/Library/Application Support/com.lore.app/Vault,
plus Open Folder… to point Lore at any directory — a git repo, an existing
Obsidian vault, a Dropbox folder. The switcher sits at the top of the sidebar
with a recents list.
Switching clears items, collections, selection, search and view before re-hydrating: ids do not mean the same thing in another folder. Only the default vault is ever seeded — writing sample notes into a folder you chose would be hostile.
<vault>/
.lore/
.gitignore committed — ignores the derived files below
collections.json committed — folder colours and order
workspace.json committed — schema version, tag order
index.db ignored — derived, deletable, rebuilt on open
cache/ trash/ ignored
attachments/ reserved; never a collection
Reading List/
how-linear-builds-product.md
Work/
some-unfiled-note.md ← the vault root means "uncollected"
Folders are collections. collectionId is never written to frontmatter — it
is derived from the parent directory, so moving a file in Finder refiles it in
Lore and there is only ever one source of truth. Items with no collection sit at
the vault root.
---
id: 01J8ZQ4F7K3M8YB2VNXR6TWDPA
type: link # link | note | task | code | image
title: How Linear builds product
url: https://linear.app/blog/how-linear-builds-product
created: 2026-08-30T10:12:04.000Z
updated: 2026-08-31T09:00:00.000Z
tags: [product, research]
inbox: true # flags are flat and omitted when false
related:
- '[[building-a-second-brain]]'
---
The Markdown body — the note's content, or your own notes on a link.Paste a GitHub repo and Lore reads its README; paste a blob/…/*.md URL and it
reads that file. Either way you get a real document to read, not a bookmark.
---
title: emitsignal/emitsignal
type: link
url: https://github.com/emitsignal/emitsignal
source:
kind: github
raw: https://raw.githubusercontent.com/emitsignal/emitsignal/HEAD/README.md
ref: HEAD
fetched: 2026-09-09T10:12:04.000Z
---The body is the Markdown itself, cached in the file — so it reads offline, is
found by search, and diffs in git. Relative images and links are rewritten to
absolute URLs once, when it is fetched, which is what makes the copy correct in
Obsidian and on GitHub too. ref: HEAD follows the repository's default branch
rather than pinning a name, and needs no API call and no token.
source is what makes the body read-only, and it stays that way: a virtual
document is a live document, not a copy you take over. Opening the item checks
the origin — at most once every few hours, and only rewriting the file when the
Markdown actually changed, so an unchanged upstream never churns your history.
Delete the source block by hand and the file is an ordinary note again.
A plain folder of Markdown just works. A folder with no collections.json
entry is still a collection, coloured from a hash of its name. A file with no
frontmatter is a valid note; its title falls back to the first heading, then the
filename.
Three processes, with one seam between them.
| Owns | |
|---|---|
| Renderer (React) | UI, Zustand store, view selectors, preferences in localStorage |
| Data engine (Bun/Elysia sidecar) | The vault files, YAML, the SQLite/FTS5 index, the file watcher, link metadata |
| Rust/Tauri | Windows, tray, ⌥Space, spawning and supervising the engine, the folder picker |
Everything the UI does goes through a single KnowledgeRepository
(src/data/repository.ts), so the app is agnostic to where data lives:
| Implementation | When it's used | Role |
|---|---|---|
VaultRepository (vaultRepository.ts) |
inside Tauri | source of truth — Markdown files, over HTTP to the engine |
MemoryRepository (memoryRepository.ts) |
Vite preview, unit tests | seed-backed |
LocalRepository (localRepository.ts) |
legacy | SQLite; kept only so the one-shot import can read an old lore.db |
src/data/index.ts picks the implementation and owns the teardown a workspace
switch needs.
A compiled Bun binary shipped as a Tauri externalBin and spawned by Rust — the
webview is granted no shell: permission, so nothing in the renderer can start a
process. It binds 127.0.0.1 on an ephemeral port and prints one handshake line;
Rust reads it and hands the renderer an endpoint plus a bearer token.
listItems() deliberately omits bodies — it re-runs after every mutation, so
shipping every body through it would serialize the whole vault on each
keystroke-triggered save. getItem() is the only route that returns one, and the
store keeps the selected item in detail.
Search goes to FTS5 for queries of three characters or more, which is what lets it reach text the list pane cannot see; shorter queries use the client-side filter over already-loaded titles.
The watcher reconciles the index before it notifies, and recognises the engine's own writes by content hash — several filesystem events for one write are all suppressed, while someone else's edit lands with a different hash and gets through.
In development the engine runs separately under bun --watch on a fixed port, so
editing it never triggers a Rust rebuild.
main— the three-pane knowledge base (sidebar · list · detail/chat).capture— the frameless Quick Capture panel toggled by ⌥Space (registered insrc-tauri/src/lib.rs; window control incommands.rs).
The capture window is a separate webview with its own repository instance, so it
follows a workspace:changed broadcast — otherwise the next capture after a
workspace switch would land in the folder you just left.
On a launch where the vault is empty, Lore imports a legacy lore.db and renames
it to lore.db.premigration — never deletes it. The guard is the vault's actual
state, not a "have I migrated" flag: a flag can be set by an attempt that then
failed, and the cost of that is a whole library stranded in a database the app no
longer reads.
Changing the bundle identifier moves the entire app-data folder, so it is a data migration and not a rename. The import only looks in the current one.
AI is pluggable (src/ai/aiProvider.ts). The deterministic MockAiProvider
powers summaries, tag suggestions, type detection and the "Ask Lore" chat with
zero network or keys. A real provider belongs in the data engine, so the API key
never reaches the renderer — not yet built.
Settings is a modal sheet (components/settings/) with the design's ten
panes. Colour mode, theme style, accent, list density, text size, AI location
and the full switch set write through the store and persist; panes that need a backend
(devices, billing, calendar accounts) render the design's copy against
placeholder figures.
Styling is Tailwind CSS v4, with src/theme/tailwind.css as the single
stylesheet. Two deliberate departures from stock Tailwind: every length is px
rather than rem (App.tsx scales the tree with zoom for the Text size
preference), and there is no dark: variant — the colour tokens are swapped
wholesale from the theme registry in theme/themes.ts, so bg-surface is
already correct in every theme. See AGENTS.md for the full
conventions.
src/
App.tsx main KB window shell
capture.tsx Quick Capture window entry
components/
common/{Icon,glyphs} SVG icon sets
common/LoreMark logo mark + serif wordmark font
kb/ TitleBar, Sidebar, WorkspaceSwitcher, ListPane,
DetailPane, AiSummaryCard, RelatedCards,
AskLoreChat, Notice
capture/ CaptureApp, CommandBar (A), Composer (B)
onboarding/ first-launch sheet (sign in / local vault / magic link)
settings/ modal sheet, ten panes, shared controls
data/ repository seam; vault (HTTP) + memory impls,
sidecarClient, derive, migrateSqlite, legacy SQLite
store/ types, seed, typeMeta, views (selectors), useStore,
persisted (prefs, auth, workspaces)
ai/ AiProvider + MockAiProvider
lib/ format, capture helpers, workspace, linkMetadata, cn
theme/ tailwind.css, tokens, bootstrap
sidecar/ the data engine (Bun + Elysia)
src/markdown.ts frontmatter ⇄ Item
src/vault.ts path safety, folders-as-collections, .lore/
src/links.ts wikilink parse / resolve / rewrite
src/index/{db,store}.ts SQLite + FTS5 index and the vault operations
src/watch.ts file watching, debounce, self-write suppression
src/routes.ts the HTTP surface
scripts/build.ts compiles the per-platform binaries
src-tauri/
src/{lib.rs, commands.rs} plugins, capture shortcut, capture window control
src/mode.rs which Lore this binary is (see "Build modes")
src/sidecar.rs spawn, handshake, supervise and kill the engine
tauri.conf.json windows, externalBin, bundle config
tauri.{dev,agent}.conf.json name, identifier and icons for a labelled build
capabilities/default.json permission grants
icons/app-icon.svg Dock icon master — regenerate the rasters with
`pnpm tauri icon src-tauri/icons/app-icon.svg -o src-tauri/icons`
icons/tray-icon.svg menu-bar master (monochrome template, auto-inverts)
icons/{dev,agent}/ tinted sets, written by `scripts/mode-icons.mjs`
lore.modes.json the build-mode table every side reads
scripts/tauri.mjs `pnpm tauri` with the mode's config applied
docs/images/ README screenshots, rendered from the design sources
pnpm test # both suites
pnpm test:sidecar # the data engine aloneThe engine's suite runs against real temporary vaults on disk — round-tripping frontmatter, folder⇄collection mapping, wikilink preservation and healing, incremental reindexing when mtimes lie, rename with link rewriting, and the path-traversal guard on the HTTP boundary.
The knowledge base, quick capture, onboarding, the settings modal, light/dark theming and the Markdown vault are all in place, including workspaces, live updates from external edits, full-text search and the import from the legacy SQLite store.
Known gaps:
tauri-plugin-sqlis still present. It is the only reader of the legacy store; it should be dropped a release after the import has shipped, along withmigrateSqlite.ts,localRepository.tsandschema.ts.- Tag order is half-built. A vault can carry its own order in
.lore/workspace.jsonand the engine serves it, but nothing writes it — there is no reordering UI, so vaults fall back to the sample order. - No manual reindex.
POST /workspace/reindexexists with no UI, so a missed watcher event has no recovery short of deleting.lore/index.db. - AI is still the mock, and still in the renderer.
- Several settings panes show placeholder figures.
Deferred by design: the Focus and Calendar surfaces — the designs treat those as
windows of their own, so only their preference panes are built. Also nested
collections, dead-link chips in Related, and replacing the full refresh() after
every mutation with optimistic updates.
Read AGENTS.md first — it is the single source of truth for
conventions in this repo, and it covers styling, code style and the commit rules
(Conventional Commits, bun format before committing).





