LifeTrace User Guide
A comprehensive walkthrough of every feature in the LifeTrace engine. Assumes you've completed the Quickstart and have the engine running at http://localhost:9030.
Table of contents
- Architecture at a glance
- Running modes — desktop app vs CLI
- Dashboard
- Sources
- Providers
- Chat
- Journal
- Places
- Relationships
- Settings
- Privacy model
- Troubleshooting
Architecture at a glance
LifeTrace is a single Node.js engine that runs on your machine. It stores data locally in SQLite (one root database for metadata, one per-source database for corpus items — a design inspired by GUFI for scalability). Vector embeddings live in LanceDB. A small web UI is served from the same process.
~/Library/Application Support/LifeTrace/
├── lifetrace.db # root: sources, providers, journal, settings
├── sources/
│ ├── imessage-<uuid>/corpus.db
│ ├── gmail-<uuid>/corpus.db
│ └── …
└── data/vectordb/ # LanceDB for embeddings
Your data never leaves this directory unless you explicitly opt into:
- A cloud LLM provider (prompts + corpus excerpts sent for chat / journal generation)
- Reverse geocoding via Nominatim (raw lat/lng coordinates sent to nominatim.openstreetmap.org once per unique place; cached locally so it's a one-time hit)
- Remote status reporting (metadata only — counts, timestamps, source names)
Everything else is local.
Running modes
LifeTrace has two run modes:
Desktop app (Electron)
npm run electron
Launches LifeTrace as a native desktop window. The engine runs embedded in the same process. Full Disk Access (needed for iMessage / Photos) gets granted to LifeTrace.app itself rather than your terminal — much cleaner for distribution.
Storage path is the OS-correct user-data directory: ~/Library/Application Support/LifeTrace/ on macOS.
CLI (Node)
npm start # production
npm run dev # nodemon-watched
Runs the engine as a normal Node process. Open http://localhost:9030 in any browser. The exact same UI loads. Useful for development and headless deployments.
Native modules (better-sqlite3, @lancedb/lancedb) need to be compiled for the runtime that loads them. Both npm run electron and npm start chain through electron/check-rebuild.js, which detects the active target and rebuilds only when needed — you should never have to think about it. If you do, the manual escape hatch is arch -arm64 npx @electron/rebuild -f -w better-sqlite3 -a arm64.
Dashboard
The dashboard is the "what's in your corpus" view.
Trace constellation (hero)
An animated canvas showing every day-source-author combination as a dot. Horizontal axis is time, vertical bands separate source types, color encodes type, brightness distinguishes self-authored (brighter) from received. Hover any dot for the date, source, and count.
With a large corpus (100k+ items) this renders as ~2-5k dots — each represents one day of one source. The overall shape is your life: which years were busy, when you switched platforms, which relationships were heaviest.
Health score + six dimensions
A weighted geometric mean of six corpus qualities:
| Dimension | What it measures | What "good" looks like |
|---|---|---|
| Volume | Self-authored token count (log-scaled) | 500k+ tokens → 85+ |
| Modality | Shannon entropy over source types | 3+ sources with 5%+ each |
| Dialogue | % of items marked as dialogue | >30% |
| Temporal | Month coverage + gap detection | No gaps >6 months recently |
| Breadth | Topic diversity (requires embeddings) | 15+ stable clusters |
| Emotion | Sentiment variance | std dev > 0.3 |
The Suggestions section surfaces gap-closing actions — e.g., "Import chat exports to raise dialogue."
Source status, embedding status, recent traces
- Source status — sync state per source, click to trigger a sync
- Embedding status — progress bar, how many items still need embedding
- Recent traces — last 20 corpus items chronologically
Tag filter
If you have sources tagged with different labels (e.g., personal, work), the filter chips above the constellation let you scope everything on the page to a subset.
Sources
A source is a connected origin of traces. Each source has its own SQLite database under sources/<slug>/corpus.db.
Source types
| Type | Method | Status |
|---|---|---|
imessage |
Reads ~/Library/Messages/chat.db (macOS) |
Available |
gmail |
OAuth2, Gmail API, incremental sync via historyId | Available |
google_calendar |
OAuth2, event + location ingestion | Available |
apple_photos_location |
Reads Photos.sqlite, clusters geotagged photos by 30-min/200-m proximity into place visits |
Available |
google_takeout_location |
Drag-and-drop Timeline.json (on-device export) or legacy Takeout archives — handles both semanticSegments and timelineObjects formats |
Available |
local_file |
Walks a directory, ingests .md and .txt |
Available |
whatsapp / telegram / signal |
Planned (file exports) | |
outlook |
Planned (Microsoft Graph) | |
| Voice notes | Planned (Whisper transcription) |
Connecting a source
The Sources page has a button per available type. Behavior:
- Gmail / Google Calendar — initiates OAuth; first time requires you to register a Google Cloud project and paste Client ID + Secret. LifeTrace then stores refresh tokens and uses them indefinitely.
- iMessage — preflight check first. If you get a permission error, grant Full Disk Access — to LifeTrace.app if you're running the desktop app, or to the terminal if you're in CLI mode.
- Apple Photos — same Full Disk Access flow as iMessage. Reads the Photos library at
~/Pictures/Photos Library.photoslibrary/database/Photos.sqlite. Photos themselves never leave the machine; only GPS + timestamps are read. - Google Timeline — Google retired Takeout for Location History in late 2024. From inside Google Maps on your phone: Settings → Location services → Timeline → Export Timeline data → AirDrop/copy the
Timeline.jsonto your Mac → drop it into the Timeline source panel. - Local files — enter a directory path, the engine walks it and ingests all supported files.
Tags
Each source can have one or more tags (default personal). Available built-in tags: personal, work, family. Custom tags work too — type them into the inline editor. Tags drive:
- The dashboard tag-filter chips (filter all visualizations to a subset of sources)
- The journal's per-day tag mix (the prompt knows "37 work / 14 personal" for that day, helping the model name the character of the day)
- Future: scoping the persona chat to specific tags
Click Edit tags on any source row to toggle tags inline. Tags are color-coded (personal=teal, work=amber, family=pink, custom=hashed cool palette).
Sync behavior
- Each source syncs incrementally using
lastSyncAt. Gmail useshistoryIdfor efficient delta queries; iMessage usesm.date > ?. - The sync manager polls every minute. Sources with
lastSyncAtolder than 1 hour (configurable viaSYNC_INTERVAL_MSenv var) are considered stale and re-synced. - Large initial syncs (200k+ iMessages) are resumable — every 500 items the engine checkpoints
lastSyncAtso a crash doesn't restart from zero. - Wake detection: if the poll timer fires more than 3 minutes late, the engine assumes the laptop just woke and immediately triggers a sync.
Tags
Each source can have one or more tags (default personal). Tags let you:
- Filter the dashboard to show only sources with specific tags
- Keep work and personal contexts separate
- (Future) Scope the journal and chat to specific tags
Deleting a source
Sources → Remove deletes the source record, its per-source corpus database, and all ingestion logs. The vector embeddings for that source's items remain in LanceDB (they're harmless but unreferenced).
Providers
Providers are your LLM configuration. You can have multiple providers and set which one is used for chat and for embeddings.
Provider types
| Type | What it supports | When to use |
|---|---|---|
ollama |
Local models (Llama, Mistral, Phi, nomic-embed-text) |
Privacy-max. Free. Slower. |
openai |
Standard OpenAI API | High quality. Pay-per-use. |
openai-codex |
ChatGPT Pro/Plus subscription via OAuth | Use your existing subscription instead of API billing. |
openai-compatible |
Anything OpenAI-format (GLM, Groq, Together, Fireworks, vLLM, your own deployment) | Self-hosted or alternative providers. |
anthropic |
Claude models | Best reasoning; no embeddings. |
google |
Gemini | Fast and cost-effective. |
Adding a provider
Each provider has a settings panel (click Settings next to a row) where you set:
- Chat model — the model ID for the chat role
- Embedding model — the model ID for embeddings (if supported by the provider type)
- Use for chat / Use for embeddings — assign roles
Only one provider is the active chat or embedding provider at a time. Clicking "Use for chat" on a new provider automatically unsets it on any other provider.
Testing a provider
Every provider has a Test button that sends "Say hello and nothing else" to the configured model and surfaces the response or error. Use this to verify credentials and model names before relying on the provider.
Codex specifics
Codex uses the ChatGPT backend (chatgpt.com/backend-api/codex/responses), not the standard OpenAI API. The engine handles the format differences internally — you just pick the type and sign in. Codex cannot do embeddings; pair it with a separate OpenAI or Ollama provider for that role.
Chat
The Chat tab is your persona chat — a RAG pipeline that retrieves memories from your corpus, grounds the response in them, and cites sources.
- Sessions let you keep multiple conversations going
- Streaming is on by default — responses arrive token by token
- Citations appear below each response, linking to the memory IDs used
The persona is instructed to say "I don't have a memory about that" when retrieved context isn't strong enough, rather than hallucinate. You can adjust retrieval sensitivity in persona/rag.js (CONFIDENCE_THRESHOLD).
When chat doesn't work well
- Empty responses — no embedding provider configured, or embeddings haven't run yet (check dashboard → Embedding status)
- "I don't have a memory" — your corpus doesn't cover the topic; add more sources or adjust the threshold
- Wrong memories retrieved — the query doesn't match your writing style; rephrase more like how you'd write
Journal
Auto-generated daily narrative — plus weekly/monthly digests, an at-a-glance history grid, and a "themes over time" view.
The Journal page has three top-level tabs: Timeline · Threads · History.
Timeline (Day / Week / Month)
The default tab — a vertical scroll of entries grouped by year. The scope toggle changes resolution:
- Day — one entry per day, the most-recent at the top. Includes search box and tag filter chips above the list. Pagination: 30 entries at a time, Load older entries at the bottom.
- Week — weekly digest entries (ISO Mon–Sun weeks). Each digest is built from the daily entries within that week.
- Month — monthly digest entries.
Each daily entry has:
- Auto-generated narrative (2–3 sentences, second person, in your voice)
- Tag chips (3–5 LLM-extracted themes per day, e.g.
work,family,travel) - Stats footer: trace count, contact count, source list
- Regenerate, Add note, Remove buttons
Tag chips are clickable — clicking one opens the Threads tab focused on that tag.
How daily entries are generated
Every morning, the sync manager checks whether yesterday has an entry. If not and the journal is enabled, it:
- Fetches all corpus items with
date(occurred_at) = yesterday - Groups by thread and contact, extracts short excerpts (prefers self-authored text)
- Pulls the persistent About-you profile for activity attribution
- Pulls the 6-day lookback so the model can recognize trip arcs vs. departures from routine
- Asks your configured chat provider to write 2–3 sentences plus a
Tags: [...]line - Saves the result to
daily_entries
Empty days (fewer than 3 corpus items) skip the LLM call entirely and are marked empty.
Status states: pending · generating · ready · empty · failed. Failed entries auto-retry up to 3 times with exponential backoff before giving up — most transient LLM errors recover without user intervention.
Threads
A reading of the themes across your corpus, not the days themselves.
Insight cards (top of tab):
- Constants of your life — tags appearing on 50%+ of days (
family,work) - Episodic chapters — meaningful but bounded in time (
travel,wedding) - Emerging recently — ≥70% of occurrences within the last 30 days (
cabo,interviews)
Heatmap (below): tags as rows, time buckets as columns, cell shading = occurrence count. Granularity toggle: Daily / Weekly / Monthly. Range toggle: 30d / 90d / 1y / All. Click a tag label on the left to focus the view on co-occurring threads.
The Threads tab is intentionally one consolidated view — the older Lanes / bump-rank / heatmap split was redundant.
History grid
A GitHub-contribution-style heatmap of every day in the corpus. One row per year, columns are weeks, cells are days. Cell shading encodes trace volume (logarithmic, normalized). Cells with a journal entry get a teal outline.
Tag-highlight chips above the grid — click a tag to highlight every day where it appeared (amber border).
Click any cell to jump back to the Timeline at that day.
The History grid scales the long view of a corpus that goes back 14+ years (iMessage often does) — vertical year-on-year stacking lets you compare months across years at a glance.
Backfill historical entries
The auto-scheduler is forward-only — it generates yesterday's entry overnight. To populate older days, the + Backfill button on the Timeline tab opens a date-range picker with two actions:
- Fill missing days — generates entries only for days that don't already have one. Cheap, additive. Use this for first-time backfill.
- Regenerate all — overwrites every entry in the range. Confirmed via prompt because it costs N LLM calls. Use this after editing your About-you profile, switching chat providers, or when the prompt has changed.
The picker max defaults to yesterday (today's day is incomplete and any mid-day generation would miss the rest of it). Server-side guards reject future dates.
A long backfill (e.g. Jan 1 → today, ~115 days) takes ~10–15 minutes at typical LLM speeds. Progress shown in a panel above the timeline; Cancel stops it cleanly.
About-you profile
Persistent context for the journal prompt. Without it, the model can confuse your partner's interview for yours, or family members' appointments for events you attended.
Settings → About you — a free-text profile injected into every journal generation.
- Click Generate suggestion from corpus to draft a starter profile from your existing journal entries + top contacts. The LLM names who's who, identifies ongoing situations, and writes attribution rules ("Partner X is interviewing — those are HER events, not the user's").
- Edit anything that's wrong and Save.
- After saving, run Regenerate all on the date range you care about so existing entries pick up the profile.
The profile sits above all per-day USER NOTES in the prompt. Per-day notes still override it for that specific day.
Search + tag filter
Above the Timeline (Day scope only):
- Search box — substring match against
daily_entries.content(LIKE, no FTS yet). Filters the visible entries client-side after the fetched batch. - Filter chips — clicking a tag narrows the list to days where that tag was extracted. Click again to clear.
Places
Spatial map of where you've been. Combines GPS visits from Google Timeline + clustered geotagged photos from Apple Photos into a single layer.
- Map — Leaflet + dark CartoDB tiles, full-canvas (sidebar collapses around it). Marker clustering with custom teal-glow bubbles that scale to 1k+ points (
1.2kabbreviated for huge clusters). - Traces — faint chronological polylines connect same-day visits. Big time/space gaps split the line so a flight from Chicago to Florida doesn't draw a straight line across the map.
- Tag filter (top-right) — chips for personal/work/family/etc. let you see only the geography of one part of your life.
- Time scrubber (bottom) — preset buttons (30d / 90d / 6mo / 1y / All) plus two date inputs for custom ranges.
- Day panel — clicking a marker slides in a panel with the date, full journal entry for that day, and an inline list of every visit that day. Open in Journal link jumps to the Timeline at that date.
Reverse geocoding
Coordinates aren't very useful as text — the LLM doesn't know 41.91, -87.65 is Lincoln Park, Chicago. The engine ships with a Nominatim-backed reverse geocoder:
- 1 req/sec rate limit (per Nominatim's policy)
- Persistent SQLite cache (
geocode_cachetable), keyed on rounded lat/lng (~110m granularity) so nearby visits share lookups - After geocoding, every location item's
content_textis rewritten with the human-readable place name, and items are flagged for re-embedding so RAG matches on neighborhood/city queries
Trigger the backfill once per source addition: POST /api/locations/geocode. A library of ~1000 unique places takes ~20 minutes the first run, then resolves instantly from cache.
Privacy
Coordinates leave the machine to nominatim.openstreetmap.org. No other data — no timestamps, no source IDs, no user identity. Each lookup is one cached request. If you need a fully offline path, swap in a local reverse-geocoder dataset (cities1000, or an MBTiles + spatialite setup); the lookup function is one file (util/geocode.js).
Relationships
Derived from your conversational corpus. Each unique threadId with dialogue becomes a relationship, annotated with:
- Total messages and sent vs received counts
- Duration (first to last message)
- Messages per day (activity intensity)
- Reciprocity (sent ÷ total) — 0.5 is balanced, <0.3 is "you listen", >0.7 is "you talk"
- Days since last message — how active the relationship is now
- Contact name — resolved from macOS Address Book when possible
The visualization is a force-directed D3 graph: you're the center, relationships orbit, edge thickness = frequency.
Settings
Daily journal settings
Toggle the auto-generation feature. Disabling pauses the scheduler but keeps existing entries visible in the Journal tab.
About you
Persistent profile injected into every journal generation. See About-you profile above for the full description and the Generate suggestion from corpus flow that drafts an initial profile from your existing entries.
Remote status
Check your corpus state from anywhere, even when the Mac is asleep. Architecture:
- Engine generates a random 32-byte key on first boot (stored in
settingstable) - When enabled, every 5 minutes the engine POSTs a metadata snapshot to
lifetrace.io/api/heartbeatwith the key in theX-Status-Keyheader - The remote server stores the snapshot under
sha256(key)[:16] - To view: open lifetrace.io/status and paste your key
What's sent: source names, types, counts, sync state, engine ID, uptime. Contacts/topics/message content are never transmitted.
Self-hosting the status server: Run lifetrace-server yourself, point your engine at it with the Custom server field. See lifetrace-server/ in the repo.
Engine info
Shows your engine ID (defaults to hostname) and data directory path. Useful for backups — if you copy the data directory to another machine, the engine there picks up the same corpus.
Privacy model
The threat model LifeTrace is designed against:
| Concern | How LifeTrace addresses it |
|---|---|
| Cloud services reading your data | Engine runs locally. Storage is local SQLite. |
| LLM provider logging your prompts | You choose the provider. Ollama runs on your machine (no prompts leave). Codex/OpenAI see only what you send. |
| Data loss when laptop breaks | Nothing automatic. Copy ~/Library/Application Support/LifeTrace/ to a backup. Time Machine covers this. |
| Family/ex seeing your Mac | FileVault protects at rest. Engine has no auth; anyone on your Mac with the engine running can see the web UI. |
| Surveillance of remote status | The status server sees metadata only. Data is keyed by a hash you control. Self-host if you want zero third-party exposure. |
| LLM hallucinating about your life | RAG retrieval with a confidence threshold; persona instructed to abstain. |
What data is sent where:
- To your LLM provider (chat): recent retrieved memories + your query
- To your LLM provider (journal): the day's corpus excerpts + contact names + your About-you profile
- To nominatim.openstreetmap.org (geocoding): raw lat/lng coords, one lookup per unique place, cached forever after
- To lifetrace.io (status): metadata only, no corpus content
- Everywhere else: nothing. Zero outbound calls by default.
Troubleshooting
Engine won't start
- Port conflict:
PORT=9031 npm startto change port - Node version: requires Node 18+
- Dependencies:
rm -rf node_modules && npm install
iMessage sync is slow / crashes
- First sync with 100k+ items is slow (10-30 min). Watch progress in the engine logs (
iMessage: 5000 processed, 3241 new). - Crashes resume from the last 500-item checkpoint. Just restart the engine.
Gmail OAuth fails
- The Gmail API must be enabled in your Google Cloud project (this is separate from creating OAuth credentials)
- Redirect URI must exactly match
http://localhost:9030/api/auth/gmail/callback - If you see "Gmail API has not been used", the engine redirects you to the exact enable URL
Chat says "No chat provider configured"
- Go to Providers, verify at least one provider is marked Use for chat
- Test the provider; if test fails, fix credentials before trying chat
Journal says "Journal is not enabled"
- Settings → Daily journal → Enable
- Remember forward-only: entries generate for days on or after the enable date
Reset everything
rm -rf ~/Library/Application\ Support/LifeTrace
Restart the engine; it recreates a fresh database. No migrations — schema changes are picked up on next boot.