SparseTree uses a hybrid storage model with SQLite as the serving layer and JSON files as the raw data cache.
┌─────────────────────────────────────────────────────────────────┐
│ Layer 3: Local Overrides │
│ User edits that take precedence and survive provider re-sync │
│ (SQLite: local_override) │
├─────────────────────────────────────────────────────────────────┤
│ Layer 2: Normalized Data │
│ Extracted facts, relationships, life events in SQLite │
│ (person, life_event, note, parent_edge, etc.) │
├─────────────────────────────────────────────────────────────────┤
│ Layer 1: Raw Provider Cache │
│ Immutable API responses from FamilySearch, Ancestry, etc. │
│ (data/person/*.json) │
└─────────────────────────────────────────────────────────────────┘
data/
├── sparsetree.db # SQLite database (serving layer)
├── person/ # Raw FamilySearch API responses (source of truth)
│ └── {fsId}.json
├── blobs/ # Content-addressed media storage
│ └── {hash[:2]}/
│ └── {hash}.{ext}
├── augment/ # Rich augmentation data (Wikipedia links, etc.)
│ └── {fsId}.json
├── favorites/ # Legacy favorites (migrated to SQLite)
├── credentials.json # Encrypted provider credentials (git-ignored)
├── browser-config.json # Browser automation settings
├── provider-config.json # Provider enable/disable settings
└── .data-version # Migration tracking
SparseTree uses canonical ULIDs as primary identifiers, with provider-specific IDs mapped via the external_identity table:
FamilySearch ID ──┐
Ancestry ID ──────┼──> external_identity ──> canonical ULID ──> person
WikiTree ID ──────┘
- Canonical IDs: 26-character ULIDs (e.g.,
01HRJK7E8X...) - owned by SparseTree - External IDs: Provider-specific (e.g., FamilySearch
KWZJ-VKB) - Bidirectional lookup: API routes accept either format
Core tables in data/sparsetree.db:
| Table | Purpose |
|---|---|
person |
Canonical person records (ULID primary key) |
external_identity |
Maps provider IDs to canonical IDs |
parent_edge |
Parent-child relationships |
spouse_edge |
Marriage relationships |
life_event |
All GEDCOM-X fact types (birth, death, occupation, military, titles, etc.) |
note |
Life sketches, stories, research notes |
local_override |
User edits that survive re-sync |
claim |
Extensible facts with provenance |
source_citation |
Source references for facts |
database_info |
Root entries and metadata |
database_membership |
Which persons belong to which trees |
favorite |
Favorited persons with tags |
blob / media |
Content-addressed photo storage |
person_fts |
FTS5 full-text search index |
Full schema: server/src/db/schema.sql
SparseTree captures all GEDCOM-X standard fact types plus FamilySearch-specific extensions:
Vital Events: Birth, Death, Burial, Cremation, Christening, Baptism
Religious: Confirmation, Religion, Ordination, Bar/Bat Mitzvah
Family: Marriage, Divorce, Annulment, Adoption
Occupation: Occupation, Education, Retirement, Apprenticeship
Military: MilitaryService, MilitaryAward, MilitaryDischarge
Residence: Residence, Immigration, Emigration, Naturalization
Legal: Census, Will, Probate, LandTransaction, NationalId
FamilySearch Custom: TitleOfNobility, LifeSketch, CauseOfDeath, TribeName, Clan
Type constants: shared/src/fact-types.ts
The person_computed view provides pre-calculated fields for AI-friendly search:
SELECT * FROM person_computed
WHERE age_at_death < 30
OR title_of_nobility IS NOT NULL
OR military_service LIKE '%general%';Available computed fields:
age_at_death- Calculated lifespanchild_count- Number of known childrenfirst_marriage_year- Year of first marriageage_at_first_marriage- Age when first marriedtitle_of_nobility- Noble/royal titlesprimary_occupation- Main occupationmilitary_service- Military service summaryhas_life_sketch- Boolean for biographical notes
- Fetch person from FamilySearch API
- Store raw JSON in
data/person/{fsId}.json(immutable cache) - Extract normalized data to SQLite tables
- Register external ID mapping
- Download photos to blob storage
- User makes edit in SparseTree UI
- Edit stored in
local_overridetable - UI shows override value, original preserved
- Override survives re-download from provider
- Compare local data with provider cache
- Open provider edit page via Playwright
- Pre-fill form with local values
- User reviews and submits