reposql
v0.1.5
Published
A Git-native database that lives in your repository
Maintainers
Readme
RepoSQL
A Git-native database. Your data lives as plain-text XML files inside your repository, compiles to SQLite on demand, and synchronizes through normal git push and git pull workflows.
No server. No second remote. No separate sync strategy.
The Problem
For small teams, the code lives in Git — versioned, collaborative, mergeable. But the database lives somewhere else. It needs a server, credentials, and a separate workflow. None of the existing options let you do the thing that feels obvious: git pull and have your database updated.
- SQLite in Git — binary file, not mergeable, conflicts destroy data
- SQL dump files — manually maintained, easy to forget
- Hosted databases — external dependency, costs money, breaks the "everything in one repo" rule
How It Works
RepoSQL has three layers:
- A flat folder of XML files — your source of truth, lives in Git, human-readable
- A build step — reads those files and compiles them into a local SQLite database
- Git hooks — automate the compile step so it's invisible
Your data is plain text. Git can diff it, merge it, and version it like any other file. SQLite is just the runtime view of that data — rebuilt on demand, never committed.
Install
npx reposql initNo global install required. Run this once in any git repository.
Quick Start
# Set up RepoSQL in an existing git repo
npx reposql init
# Edit db/schema.xml to define your tables, then build
npx reposql build
# Start of day — get teammates' changes
git pull
npx reposql sync
# End of day — share your changes
npx reposql push
git add db/records/
git commit -m "what you did"
git pushRepository Layout
my-project/
├── db/
│ ├── schema.xml ← table definitions
│ └── records/ ← one XML file per record (committed)
│ ├── 01ARZ3NDEK...xml
│ └── 01BX5ZZKBK...xml
├── .reposql/
│ ├── config.json ← source definitions (committed)
│ └── merge-driver.js ← last-write-wins conflict resolution
├── .gitattributes ← registers the merge driver
├── .gitignore ← ignores app.db and state.json
└── app.db ← SQLite database (local only, never committed)Schema
Define your tables in db/schema.xml:
<schema>
<table name="users">
<column name="name" type="TEXT" />
<column name="email" type="TEXT" unique="true" />
<column name="role" type="TEXT" default="member" />
</table>
<table name="products">
<column name="name" type="TEXT" />
<column name="price" type="REAL" />
<column name="stock" type="INTEGER" default="0" />
</table>
</schema>Supported column attributes: type, unique, default, not_null.
Record Format
Every piece of data is stored as a single XML file named by its ULID:
<!-- db/records/01ARZ3NDEKTSV4RRFFQ69G5FAV.xml -->
<record>
<meta>
<table>users</table>
<operation>insert</operation>
</meta>
<data>
<name>Alice</name>
<email>[email protected]</email>
<role>admin</role>
</data>
</record>Updates and deletes are new files that reference the original:
<record>
<meta>
<table>users</table>
<operation>update</operation>
<target>01ARZ3NDEKTSV4RRFFQ69G5FAV</target>
</meta>
<data>
<email>[email protected]</email>
<name>Alice</name>
<role>admin</role>
</data>
</record><record>
<meta>
<table>users</table>
<operation>delete</operation>
<target>01ARZ3NDEKTSV4RRFFQ69G5FAV</target>
</meta>
</record>Write API
Use the write API so your app doesn't have to create XML files manually:
import { RepoSQL } from 'reposql'
const db = new RepoSQL('./db')
// Insert — returns the new row's ULID
const id = db.insert('users', {
name: 'Alice',
email: '[email protected]',
role: 'admin',
})
// Update
db.update('users', id, { email: '[email protected]', name: 'Alice', role: 'admin' })
// Delete
db.delete('users', id)
// Query (standard SQL via better-sqlite3)
const users = db.query('SELECT * FROM users WHERE role = ?', 'admin')
const alice = db.queryOne('SELECT * FROM users WHERE email = ?', '[email protected]')
// Access the raw better-sqlite3 instance for advanced queries
db.db.prepare('SELECT COUNT(*) as n FROM users').get()
db.close()Each call writes an XML record file and updates the local SQLite database immediately.
Your app can also query SQLite directly without the write API:
// Node.js
import Database from 'better-sqlite3'
const db = new Database('./app.db')
const users = db.prepare('SELECT * FROM users').all()# Python
import sqlite3
conn = sqlite3.connect('./app.db')
users = conn.execute('SELECT * FROM users').fetchall()CLI Reference
| Command | Description |
|---------|-------------|
| npx reposql init | Set up RepoSQL in a git repository |
| npx reposql build | Rebuild SQLite from scratch |
| npx reposql sync | Apply new XML records to SQLite (run after git pull) |
| npx reposql push | Export SQLite changes as XML record files (run before git push) |
| npx reposql status | Show unsynced records and local SQLite changes |
| npx reposql log | Human-readable history of all data changes |
| npx reposql migrate | Apply schema.xml changes to SQLite |
| npx reposql snapshot | Collapse records into a snapshot and archive history |
Options
npx reposql build --quiet
npx reposql sync --quiet
npx reposql push --dry-run
npx reposql snapshot --dry-run
npx reposql log --table users
npx reposql log --author [email protected]
npx reposql log --since 2026-01-01
npx reposql log --limit 50
# Multi-source: target a specific database
npx reposql build --source products
npx reposql sync --source logs
npx reposql status --source usersMultiple Databases
A single repository can contain multiple independent databases — each with its own schema, records directory, and SQLite file.
Setup
# First database (default)
npx reposql init
# Additional databases
npx reposql init --db analytics/data.sqlite --name analytics
npx reposql init --db audit/logs.sqlite --name auditThis registers each source in .reposql/config.json:
{
"sources": {
"default": { "dbDir": "db", "sqliteFile": "app.db" },
"analytics": { "dbDir": "analytics", "sqliteFile": "analytics/data.sqlite" },
"audit": { "dbDir": "audit", "sqliteFile": "audit/logs.sqlite" }
}
}Running commands across sources
# All commands run for ALL sources by default
npx reposql sync
npx reposql build
npx reposql status
# Or target a specific source
npx reposql sync --source analytics
npx reposql build --source audit
npx reposql status --source defaultEach source maintains its own sync cursor and baseline independently. State is stored per-source in .reposql/state.json (gitignored).
Day-to-Day Workflow
# Start of day
git pull
npx reposql sync # apply teammates' record files to your SQLite
# ... build your feature, your app reads/writes SQLite normally ...
# End of day
npx reposql push # export your SQLite changes as XML record files
git add db/records/
git commit -m "add user alice, update product price"
git pushTwo extra commands. Everything else stays the same.
Conflict Resolution
Because every record is its own uniquely-named file, two teammates can never conflict on the same file during normal use. You never modify existing files — you always append new ones.
In the rare case where the same file is modified in both branches, the registered merge driver resolves it automatically using last-write-wins (higher ULID = newer = kept). The developer never sees a conflict.
# .gitattributes (written by `reposql init`)
db/records/*.xml merge=reposql-driverSnapshots
When db/records/ grows large, collapse history into a single snapshot:
npx reposql snapshotThis writes the current state to db/snapshot.xml, archives old record files to db/archive/YYYY-MM/, and clears db/records/. Future builds load the snapshot first, then replay any new records on top. Teammates get the snapshot on their next git pull.
Schema Changes
To add a column, update db/schema.xml and run:
npx reposql migrateThis applies ALTER TABLE ADD COLUMN for new columns and validates existing records. Column drops require a full npx reposql build.
Known Limitations
- Not for high write concurrency — designed for small teams, not APIs with thousands of writes per second
- Not for large datasets — comfortable up to ~50,000 records before needing the snapshot strategy
- No real-time sync — data syncs on
git pull, not live. This is intentional. - Last-write-wins is lossy — if two teammates update the same record simultaneously, one change is lost. Acceptable for small teams, wrong for financial or critical data.
Comparison
| | RepoSQL | SQLite in Git | Dolt | Hosted DB | |-----------------------|:-------:|:-------------:|:----:|:---------:| | Lives in Git repo | ✓ | ✓ | ✓ | | | One remote (GitHub) | ✓ | ✓ | | | | Mergeable data | ✓ | | ✓ | | | SQL queries | ✓ | ✓ | ✓ | ✓ | | No server required | ✓ | ✓ | ✓ | | | Full audit history | ✓ | | ✓ | | | Human-readable diffs | ✓ | | ✓ | | | Small team friendly | ✓ | ✓ | ⚠ | ⚠ |
License
MIT
