history
I wanted to summarize the experience of building lifelab from scratch. I asked claude to put a story-arc based on my (it's) git history. I thought it's interesting to put the commits on a timeline and find distinct patterns (eras) of development. It actually weaved a fun story. Since Claude actually writes proper commit messages and PR you can backtrace the entire system and most decisions. I definitely wish I had the discipline to do that manually and I wonder how many production systems do.
[slop disclaimer]
The content after block this is fully claude generated. I am not sure about the etiquette of exposing readers to slop so I decided to put a warning . I left some human comments, but I was entertained by what claude wrote enough to share it without major editing. Maybe the quality went up compared to my human writing?
LifeLab's git history reads like a speedrun of software philosophy. It starts as a clone ("what if Deepnote but mine?"), immediately rebels against its parent ("not notebooks — pages"), discovers its spiritual ancestor ("org-mode had it right"), goes through an infrastructure gauntlet ("Docker on Hetzner, fight me"), tries four different scripting runtimes before settling on one, builds its own extensibility model from scratch, and ends up as something genuinely novel — a malleable, org-mode-inspired personal knowledge OS that runs as a desktop app, PWA, or self-hosted server, with AI woven into the fabric.
The claude/ branch prefixes tell their own story: this was built through relentless human-AI pair programming, with the human driving the vision and the AI executing at a pace that produced 15 commits a day for over four months straight.
The first commit: Add notebook-app package
The repo starts as a fork/clone of Deepnote — a computational notebook platform. The very first PR is already from claude/ — AI-assisted from day zero. Within hours, it's already being pulled apart: cell buttons move, the Deepnote Toolkit gets integrated, then immediately reverted and removed. The codebase is being interrogated: what are you, and what should you become?
Human note
I saw deepnote being open sourced on hackernews and instantly thought I can reuse it for note taking. I have been working on a jupyterlab-task-management solution but the interface was ugly, and deepnote was pretty. I quickly realized not all parts were shared and the parts I wanted were proprietary.
:PROPERTIES:(14 fields)
Key commit: Refactor: Convert to standalone meta-programmable notebook app
This is the fever-dream opening. 100 commits on November 6 alone. The project explores:
- Malleability system — hooks, self-documentation, "the blocks should modify themselves"
- Schema blocks — JSON editors for structured data
- CSS variables unification — full theming system
- WebSocket race conditions — the inevitable async debugging
- A Rust backend investigation alongside the Python one (Axum + DashMap)
- A ratatui TUI (terminal interface!) gets built in parallel
- Vim-style command mode with autocomplete in the TUI
- The commit messages capture the energy: yeeeee, push all, run. Someone is building very fast and seeing what sticks.
Human note
The first system was a crystalization from blog/pkm/PKM concept exploration]. I had an idea of everything being a block, including code blocks that can query the database. The most unique thing is I went with rust for rest backend instead of python fastapi. I was familiar with fastapi, so I could fix it myself. I decided I don't want to fix things myself. Rust Axum ended up being fantastic, it was performant, snappy and once claude got the code to compile it was basically bug free
:PROPERTIES:(14 fields)
Key commit: Refactor: Rename notebooks to pages throughout codebase
A philosophical pivot: the atomic unit isn't a "notebook" — it's a page. Notebooks are linear; pages are a graph. This era introduces:
- Hierarchical page titles (folders removed — just use / in names)
- Tags as links between concepts
- Fractional indexing for block reordering
- pgvector embedding storage for semantic search
- Fulltext search with configurable filters
- Task blocks with dedicated UI
- The naming is still "Deepnote" in the repo URL, but the soul has changed.
Human note
I hate folders and wanted to enforce purity. I messed up by naming tags "links" just to be unique, no one including me could get it.
:PROPERTIES:(14 fields)
Key commit : Complete rebrand from Deepnote to LifeLab
The project gets its real name. This isn't just cosmetic — it marks the shift from "a notebook tool I'm hacking on" to "a personal knowledge operating system." The features that follow confirm the ambition:
- Pinned blocks and wiki-style auto-page-creation
- Publishing infrastructure — static HTML blog export
- Task management with due dates, collapsible descriptions, metadata panels
- Image upload with drag-and-drop
Human note
5 days since starting it, it become a fairly usable wiki web app. The name was stupid but it stuck.
:PROPERTIES:(14 fields)
Key commits: Add complete Docker deployment setup for Hetzner with Watchtower CD, Re-implement PWA features with service worker and offline support
The build-it-and-they-will-come phase. Massive infrastructure work:
- Docker deployment on Hetzner with Watchtower continuous delivery
- PWA with iOS support, service worker, offline IndexedDB
- Telegram bot for mobile ingestion ("send a thought from your phone")
- Mobile-responsive UI with hamburger menu
- Calendar sidebar replacing the journal list
- Passkey authentication (WebAuthn) — no passwords
This era has the most Docker debugging commits you'll ever see. GLIBC mismatches, nginx regex bugs, kernel bridge paths. The classic "it works on my machine" gauntlet.
Human note
I can't believe I did all of this in two weeks this was insane. Claude code quota was very generous and I basically worked on this every night after work.
:PROPERTIES:(14 fields)
Key commits: Add smart title bar with org-mode inspired syntax, Implement single timer mode (org-mode style), Add TODO/DONE keyword-based tasks (org-mode style)
A design revelation: Emacs org-mode had figured out personal knowledge management decades ago. LifeLab starts absorbing its ideas wholesale:
- Smart title bar with org-mode headline syntax
- Property drawers — collapsible metadata panels
- TODO/DONE keywords for task management
- Tag syntax with
:trigger (:project:urgent:) - Logbook for time tracking (clock in/out)
- Org-mode export format
- Collapsible headlines with fold indicators
The CSS restyling commits (Restyle metadata and logbook as org-mode property drawers) show the UI being pulled toward org-mode's information density.
Human note
What started as "jupyter but with tasks" slowly became "org mode, but without the yucky Emacs" I really liked the information density of org mode styling blog/pkm/Beauty of orgmode syntax] and started leaning into it more. I initially left both jupyter styling and orgmode styling as an option. This is when I used lifelab every day.
:PROPERTIES:(14 fields)
Key commit: feat: add Tauri desktop app scaffolding and backend mode toggle
LifeLab goes native. A Tauri (Rust + WebView) desktop app gets scaffolded with:
- Native SQLite repository (rusqlite, not sqlx)
- FTS5 search for local full-text search
- Sync JavaScript runtime via rquickjs embedded in Rust
- Hooks and slash commands ported to Tauri mode
- GitHub Actions CI for building desktop binaries on release
- Bundled tutorials seeded on first run
The repository pattern (from CLAUDE.md) makes sense now — the frontend needs to work against REST or Tauri or PWA/IndexedDB backends interchangeably.
Human note
Multibackend era, where there are 3 implementations of database with 1 frontend
:PROPERTIES:(14 fields)
The runtime roulette: Python (Jupyter) → Rhai → JavaScript (QuickJS/rquickjs) → Lua
This is the most fascinating thread in the entire history:
- Python/Jupyter was the original runtime — full notebook execution
- Rhai (Rust scripting language) was tried, got syntax highlighting, then was removed entirely (PR #552)
- JavaScript via QuickJS was embedded in Rust via rquickjs — then also removed (PR #553: "Remove JavaScript runtime support, keep Python-only")
- Lua arrived and stuck — the
nb.*API was ported, tutorials rewritten, all JS references replaced with Lua (PR #736)
The final commit in this saga: Remove jupyter view mode, commit fully to org mode (#772) — the Jupyter notebook heritage is finally, fully shed.
Human note
Claude says it is interesting but this was the most slop heavy time. I did not find a proper way for users to extend lifelab using existing code. I wanted to be hipster by adding rhai (basically scriptable rust), but lua ended up faster. Embedding quickJS and nodeJS was a complete disaster. I thought it would be easy since I have to ship javascript anyway, but serverside executed javascript was async and basically broke everything constantly.
:PROPERTIES:(14 fields)
Era 8 — Malleable Software — The Drawer System (Mar 2026)
Key commits: Migrate logbook to system renderer with metadata-driven architecture, Add Tag Behavior editor with explicit renderer binding
The extensibility model crystallizes. Instead of hardcoded block types, LifeLab introduces:
- System renderers — installable/uninstallable UI components
- Drawer system — opt-in metadata panels that can be added to any block via tags
- CSS theme renderer — themes as installable drawer-blocks
- AI-assisted renderer authoring — Claude helps write JSX drawer components
- Tag Behavior editor — bind renderers to tags declaratively
This is the "malleable software" vision fully realized — the app can modify its own UI.
Human note
I tried decomposing the app into modules that can be reused an remixed.
:PROPERTIES:(14 fields)
Key commits: Add ultrathink toggle to AI blocks, Add inline diff overlay for AI suggestions, Add transcription block type with Soniox real-time audio
The latest era weaves AI throughout:
- AI blocks with model selection, prompt pages, inline diff overlays
- Inline AI edit —
/aicommand for quick block modifications - Transcription blocks with real-time Soniox audio input
- PDF viewer with annotation drawer, highlights, touch support, zen-mode fullscreen
- Pinyin ruby annotations for CJK characters
- UI form inputs with
nb_ctxcontext injection - Zero-downtime deployments with Caddy
And the human moments: 618a9b90 I broke things and blamed claude, 01ab449f add npm, this one is on claude
:PROPERTIES:(14 fields)
| Version | Date | Era |
|---|---|---|
| 1.0.0 | Jan 24, 2026 | First stable release — Tauri scaffolded |
| 2.0.0 | Feb 9, 2026 | Tauri desktop app with local SQLite |
| 3.0.0 | Mar 7, 2026 | Drawer/renderer system, org-mode committed |
| v3.2.2 | Mar 16, 2026 | Latest — PDF annotations, AI blocks, Lua |
- 2,053 commits in 137 days (~15/day average)
- 509 merged pull requests (778 total PRs opened)
- 1,003 commits in November 2025 alone (the founding month)
- 100 commits on November 6 — the single highest day
- 4 scripting runtimes tried (Python, Rhai, JS, Lua)
- 3 major UI paradigms (notebook → pages → org-mode)
- 3 deployment targets (Docker/cloud, PWA, Tauri desktop)
The Wrong Turns, Dead Ends & Lessons
Lifelab had 13 explicit reverts, at least 6 major feature removals, and a few commits that read like cries for help.
1. The Scripting Language Roulette (biggest wrong turn)
Timeline: Python/Jupyter (Nov 2025) → Rhai (Dec 2025) → JavaScript/QuickJS (Jan 2026) → Lua (Feb 2026)
Four scripting runtimes in four months. Each one got real investment:
- Python/Jupyter — the original. Full kernel execution, SSE streaming, package management, Docker kernel bridge. Dozens of fixes for Docker path issues, GLIBC mismatches, idle transactions. It worked, but it dragged in a massive dependency (Jupyter) for what was becoming a lightweight note-taking tool.
- Rhai — a Rust-native scripting language. Got syntax highlighting, spawn/update APIs, database tests. Then PR #552: Remove Rhai scripting language support entirely. Likely too niche — who knows Rhai?
- JavaScript/QuickJS — embedded in Rust via rquickjs. Got the full
nb.*API, UI rendering, MIME outputs. Then PR #553: Remove JavaScript runtime support, keep Python-only. Two runtimes removed back-to-back. - Lua arrived and stuck. Lightweight, embeddable, familiar enough, and plays nicely with Rust (mlua). PR #736: Replace JavaScript references with Lua across codebase.
The final nail: PR #772: Remove jupyter view mode, commit fully to org mode. The Jupyter heritage — the very origin of the project — is cut.
2. The Ratatui TUI (built then killed)
Built in the first week (Nov 6–7, 2025) alongside the web frontend. Got vim-style keybindings, block reordering (yy/p), search integration, modular refactoring. It was a whole parallel UI surface.
Removed: PR #436 (remove: frontend-tui (Ratatui terminal UI)). Likely the realization that maintaining two completely separate frontends for a personal tool isn't worth the cost. The TUI was cool but duplicated every feature.
3. The PWA That Was Built Twice
PR #53 added a comprehensive PWA with iOS support and React Query migration. Then immediately reverted (PR #54: Revert "Building Responsive and Fast PWAs"). Then re-implemented from scratch — feat: Re-implement PWA features with service worker and offline support.
The first attempt apparently over-scoped (React Query migration bundled with PWA support). The second time, it was just the PWA parts.
4. Semantic Search / Embeddings (built then ripped out)
A meaningful investment:
- pgvector extension added to PostgreSQL
- MiniLM-L12-v2 embeddings, then switched to Hugging Face Nomic
- Semantic search with mode parameter (blocks vs pages)
- Docker rebuilds to get pgvector compiled
Then PR #564: Remove embeddings and semantic search across backend and nb API. Full-text search turned out to be sufficient for a personal knowledge base. The embeddings added complexity (model downloads, vector storage, GLIBC issues) for marginal benefit on a single-user corpus.
5. Auto-Tag Blocks with Journal Date (three attempts)
This one was tried three times:
- Auto-tag blocks with today's journal date on creation → Reverted
- Reapply "Auto-tag blocks with today's journal date on creation" → Reverted again
- Auto-tag blocks with today's journal date on creation (third time)
The concept is simple — when you create a block, tag it with today's date. But the implementation kept breaking something. Classic "sounds easy, surprisingly hard" territory.
6. The MCP Server Hallucination Incident
Claude (the AI) was used to build an MCP server that documented the nb.* API. Problem: Claude hallucinated method names. The fix commit is blunt:
CRITICAL FIX: Correct all hallucinated nb module methods in MCP resources
Specific corrections: nb.get_page() doesn't exist (it's nb.get_page_by_title()), nb.create_page() was documented but never implemented. An honest artifact of AI-assisted development — the AI confidently documented an API that didn't exist.
7. Config Storage → Blocks → Back to localStorage
PR #484 moved UI configs from localStorage to repository-backed tagged blocks — the idea being that configs should live alongside content and sync across devices. This created a __config page with leaked blocks visible to the user. Immediately reverted: revert config storage from blocks to localStorage to fix __config page block leak.
8. EXIF Stripping (privacy vs utility)
Image EXIF data was being used for smart date routing (photos get filed under the journal date they were taken). Then someone added EXIF stripping for privacy. But stripping EXIF before extracting the date kills the date routing. Reverted: Revert "feat: strip EXIF data from uploaded images for privacy". A classic tension between two good ideas that conflict.
9. The HybridEditor Oscillation
- PR #562: Remove HybridEditor wrapper and use CodeEditor directly
- PR #572: Revert "Remove HybridEditor wrapper and use CodeEditor directly"
An abstraction layer that seemed unnecessary until it was removed and things broke. The kind of wrong turn where you learn what the abstraction was actually protecting you from.
10. Persist Python Outputs to Database (wrong approach)
The goal: include Python execution outputs in HTML export.
- First attempt: persist outputs to the database. Reverted — probably too much mutation for what's essentially a rendering concern.
- Second attempt: execute Python blocks during HTML export instead of persisting outputs. Run them fresh at export time. Simpler.
11. "gemini fix what claude broke" × 5
Five separate commits with this exact message, scattered across different weeks. At some point, Gemini was brought in as a second opinion / cleanup crew for Claude's work. The commit messages read like a running joke. There's also a gemini cleanup commit. The human is playing two AIs against each other.
12. The Scroll Nav Animation
PR #612: Remove scroll-based minimize animation from mobile nav bar. A mobile UX pattern where the nav bar would shrink on scroll. Probably felt clever in theory but janky in practice — scroll-based animations on mobile are notoriously finicky.
13. Linked Tasks UI (nice idea, abandoned)
Add separate 'Linked tasks' section on concept pages — when you're on a concept page (like "Project X"), show all tasks linked to it. Built, shipped, then: Remove linked tasks UI and make transclusions collapsible. The transclusion system (embedding blocks from other pages) made dedicated "linked tasks" UI redundant.
14. The Human Moments
The most revealing commits aren't the features — they're the one-offs:
618a9b90I broke things and blamed claude01ab449fadd npm, this one is on claudeddd90c60remove kys (removing PowerSync private keys from the repo — abbreviated "keys")3b0e651bt checku:wq (vim muscle memory leaking into a merge commit)09191e91yeeeeecbd6d7acrun055da172push all09624983nom nom nom