LifeTrace
Docs/User guide

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

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.json to 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 uses historyId for efficient delta queries; iMessage uses m.date > ?.
  • The sync manager polls every minute. Sources with lastSyncAt older than 1 hour (configurable via SYNC_INTERVAL_MS env var) are considered stale and re-synced.
  • Large initial syncs (200k+ iMessages) are resumable — every 500 items the engine checkpoints lastSyncAt so 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:

  1. Fetches all corpus items with date(occurred_at) = yesterday
  2. Groups by thread and contact, extracts short excerpts (prefers self-authored text)
  3. Pulls the persistent About-you profile for activity attribution
  4. Pulls the 6-day lookback so the model can recognize trip arcs vs. departures from routine
  5. Asks your configured chat provider to write 2–3 sentences plus a Tags: [...] line
  6. 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.2k abbreviated 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_cache table), keyed on rounded lat/lng (~110m granularity) so nearby visits share lookups
  • After geocoding, every location item's content_text is 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:

  1. Engine generates a random 32-byte key on first boot (stored in settings table)
  2. When enabled, every 5 minutes the engine POSTs a metadata snapshot to lifetrace.io/api/heartbeat with the key in the X-Status-Key header
  3. The remote server stores the snapshot under sha256(key)[:16]
  4. 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 start to 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.


See also

Something out of date? Edit on GitHub