Under the hood
A visual tour of log(ger)’s SQLite database. Each animation below shows real table rows appearing, values flashing, and aggregates rebalancing as the corresponding operation runs in the app. Everything is local on disk — no servers, no sync.
The schema, at a glance
One SQLite file: ~/Library/Application Support/Logger/logger.db
(packaged) or ./logger.db (dev). Four tiers in the core hierarchy
and a few helpers around them.
- id, name, display_name
- color, position, is_system
- id, name, display_name, color
- group_id → category_groups
- id, name, display_name
- session_id, family_id, position
manual_entries
- id, date, duration_minutes
- category_id, description, location
exact + prefix.ai_descriptions
Full schema
Every table, every column, with types and key relationships annotated. PK primary key · FK foreign key · UQ unique · IDX indexed
Watch the database change
Each panel shows the tables that the operation touches, with rows appearing, values flashing, and aggregates re-balancing as the app commits its writes. Everything loops — let it run for a few seconds to see the full sequence.
Start a timer
You hit play on a category. A row appears in timer_entries — nothing else changes yet.
INSERT INTO timer_entries with is_active = trueStop a timer
Duration computes, the timer row updates, then observations and text_entries roll the change up.
date shifts before the upsertAdd a manual entry
You forgot to time something. Insert + upsert + append, same downstream story as stop.
Edit an entry
Change the category, the date, or the duration — and the aggregate rebalances: subtract from the old, add to the new.
Add a new family
One insert into category_families, optionally with auto-link rules.
Import a CSV pair
One file pair fans out across six tables in a single transaction.
2026_spring_text.csv
Replace the database
The file on disk is swapped. The previous DB rolls into a single backup. Schema migrations re-run on the new file.
current
candidate
rolling backup
Delete an entry
Subtract from the aggregate first, then drop the row. The narrative log stays.
Properties worth knowing
- Aggregations are stored, not derived on read.
observationsis the truth for analytics — updated transactionally with every write so charts never have to scan timer/manual tables. - Schema migrations are idempotent. Every backend start runs
init_db, which probes before it changes anything. Safe to restart at any time; safe to load an older DB. - The DB is the source of truth. Use Download current from Settings to archive a snapshot; Choose .db file to load one back.
- Late-night attribution lives at the entry, not the aggregate. A timer started at 1am that you attributed to “yesterday” writes
date = yesterdayon the entry; the observation rolls up under yesterday too. - One rolling backup. Each DB swap overwrites
logger.db.bak— if you want a longer trail, Download current first.