Lix is an embeddable change control system built on SQLite that tracks entity-level changes through a commit graph.
Lix uses SQLite as its storage layer. The Lix engine manages the database and exposes high-level views for working with files, entities, and history:
file - Query current file statesstate - Query current entity stateschange - Access individual entity changescommit - Access commit recordsversion - Work with branchesfile_history - Query historical file statesstate_history - Query historical entity statesThese views abstract the underlying database schema, providing a clean SQL interface for change control.
Commits form a directed acyclic graph (DAG) where each commit references its parent commit(s):
C4 ← C3 ← C2 ← C1
↖
C5 ← C6Key concept: Each commit contains a change set (group of entity changes) and references parent commits. This creates a graph of history that enables:
The commit graph is global and shared across all versions.
See Architecture for details on state materialisation, change set mechanics, and the formal data model.
Writing a file invokes a plugin to detect and record changes:
await lix.db
.updateTable("file")
.where("path", "=", "/example.json")
.set({ data: newFileContent })
.execute();The plugin's detectChanges method compares before/after states:
detectChanges({ before, after, querySync }) => DetectedChange[]Returns an array of detected changes:
[
{
schema: JSONPointerValueSchema,
entity_id: "/user/age",
snapshot_content: { path: "/user/age", value: 31 }
}
]For each detected change, the engine:
change record with the new entity snapshotstate view to reflect the current entity stateOld entity states remain queryable through state_history. Nothing is ever deleted - new records are added while old ones are preserved.
Versions inherit state from their commit ancestors using a copy-on-write model:
Main version at C3:
- entity A (changed in C1)
- entity B (changed in C2)
- entity C (changed in C3)
Feature version at C5 (parent: C3):
- Inherits: entity A, entity B
- Changed: entity C (modified in C5)
- New: entity D (added in C5)How it works:
Benefits:
The file_history and state_history views use two special columns:
lixcol_root_commit_id
lixcol_depth
0 = current state at root commit1 = one commit back2+ = further back in history// Get file history from the active version's commit
const history = await lix.db
.selectFrom("file_history")
.where("path", "=", "/example.json")
.where(
"lixcol_root_commit_id",
"=",
lix.db
.selectFrom("active_version")
.innerJoin("version", "active_version.version_id", "version.id")
.select("version.commit_id")
)
.orderBy("lixcol_depth", "asc")
.selectAll()
.execute();This returns all historical states of the file, ordered from current (depth 0) to oldest.
The Lix engine runs anywhere JavaScript runs:
Browser
Node.js
Workers
Why SQLite?
1. File updated
↓
2. Plugin.detectChanges() compares before/after
↓
3. Returns DetectedChange[]
↓
4. Engine creates change records
↓
5. Engine updates state views
↓
6. Engine creates commit in commit graph
↓
7. History preserved, state queryableKey insight: The engine manages all complexity. You interact through simple SQL queries against views (file, state, file_history, etc.), while the engine handles commit graph traversal, state inheritance, and change detection.