what is lifelab techie edition
Core notebook schema — a simplified view
erDiagram
pages ||--o{ blocks : contains
pages {
uuid id PK
string title
json properties
json tag_schema
json page_order
}
blocks {
uuid id PK
uuid page_id FK
string type "note, code, image, html, lens, and other media"
string title
text content
string_array tags
json metadata "User fields live in metadata.properties"
string task_status "Optional task state"
}LifeLab clients, storage, and Lua runtime
flowchart TB
subgraph Clients["Clients"]
Web["Web / PWA"]
Native["Native apps"]
Agent["External agents via Mini / MCP"]
end
subgraph Notebook["Notebook operations"]
API["Pages, blocks, and properties"]
Lua["Lua runtime<br/>nb.* queries and writes"]
Hooks["Hooks"]
Views["Portable UI JSON<br/>ui.*"]
end
subgraph Storage["Host storage"]
Max["Max: PostgreSQL"]
Local["Local hosts: SQLite"]
Media["Media storage"]
end
Web --> API
Native --> API
Agent --> API
API --> Lua
API --> Hooks
Lua --> Views
API --> Max
API --> Local
API --> MediaWhat is lifelab? Techie version
LifeLab is also two things:
*1. Ode to databases
**Dinosaur era tech
My longtime friend sixhobbits always harps about just using a database and I finally took their words literally. LifeLab is centered around a postgres database with a REST api. Everything is in a main table of blocks with jsonb metadata. No Document Databases, no CRDTs, no Graph Databases, just PostgreSQL. There are some operations but most boil down to a wrapper around SQL. The purpose was to make it work with a database and with as few tables as possible. For example, even the CSS theming is just a data block in the database. We query all css blocks and then check which one has “css_enabled” in the metadata to load it in CSS. Lua code blocks query the same notebook and emit charts and tables through the portable UI contract.
I was fairly liberal with indices to trade memory consumption for speed, but I am fairly confident the database will outlast a lifetime worth of notes with sub second queries. The closest app is trilium notes which is built around a sqlite database with embeddable javascript. I just did not like the user interface interface so much.
**Blocks is all you need
The schema is fairly simple: a table for blocks. Blocks have content, a title, tags, metadata and a block type. Text and tasks are note blocks; task state is a field rather than a separate block type. Another table for pages autogenerated from block links. A single block contains the entire cell (for example, this entire article is one block, a task is also one block). I thought about this heterogenous data; maybe it's better for each block type to have a different table? But then I would have to do multi table joins. My 2 CS degrees were not sufficient to make the call, so I opted with the conceptually simpler option. I am sorry.
**DIY RAG
Everyone was obsessed RAG and embedding databases. I caved in and added a pgvector column to add custom embeddings for pages. The idea is to evaluate various embedding approaches, models and strategies on my own data to see how it behaves, because I felt most benchmarks are rather vague. Do I average embeddings over all blocks? Do I read all blocks into a single model? What is the difference? Can we do cross modal embeddings for image pages and text pages?
*2. Over-engineered to hell
**Rust backend
LifeLab backend was vibe-written in rust despite it just being a simple web server. I could have probably written a (worse) fastapi server by myself but ended up spending $100 worth of claude code tokens to write it in rust. I initially thought for a rest API it doesn't matter, but the compiler actually found a lot of bugs and prevented claude code from getting away with slop. For vibe coded python I had to be fairly careful to review changes, but somehow for rust if it compiles, it just works?!
**Lua runtime
Notebook code now runs in Lua. The nb.* API reads and updates the same pages and blocks you edit by hand. Hooks and code blocks share this API, and their writes are applied through the host's repository.
**Analysis output
Lua code can aggregate note properties and return charts, tables, statistics and custom views through the portable ui.* contract. Python kernels, Rhai and the old JSX renderer runtime have been retired.
**Passkeys, PWA, all the bling
I have never done a fullstack app before so there was a lot of learning about authentication involved.
**O(n) OCD
I tried to make it snappy and only run computations on demand with aggressive caching. One thing I am not happy about other note taking apps is the data schema. Notion is block-based, but every character run is a block, which makes for constant database joins. A paragraph can be like 50 blocks which just feels weirdly wrong to me. (also, most block based editors have this ever so slight latency which drives me nuts.)
On the other hand, if you want to get tasks in emacs, you need to parse the text using regex. Now this is fast as fuck, but it is also O(N). Querying tasks or json objects in LifeLab and Notion is O(1) due to indexing. No parsing text files, no temporary databases, just single source of truth with some B-trees.
I tried to kill as many linear scans as possible by using SQL wrappers. I am fairly confident I am not able to implement a faster text data structure by myself. Does it matter for a text platform? Probably not. When code snippets create blocks, it is all collected by the rust endpoint and executed as a single transaction instead of blocking the database in a for loop.