# cr-sqlite: merging SQLite databases written offline

> cr-sqlite is a loadable SQLite extension that turns ordinary tables into conflict-free replicated relations. It is for teams who need offline writes and multi-writer sync without writing their own merge logic, and it costs about 2.5x insert throughput on the tables it converts.

**vlcn-io/cr-sqlite** — Convergent, Replicated SQLite. Multi-writer and CRDT support for SQLite

- Repository: https://github.com/vlcn-io/cr-sqlite
- Website: https://vlcn.io
- Stars: 3,803 · Forks: 126
- Language: Rust
- License: MIT
- Published: 2026-09-23 · Updated: 2026-09-23 · Language: en
- Canonical page: https://hysenlabs.com/projects/vlcn-io-cr-sqlite

## The merging problem cr-sqlite takes off your application

Two devices edit the same SQLite database while offline. When they reconnect, someone has to decide what happens to the rows. In most applications that someone is you: a sync layer, a last-write-wins rule, a queue of pending operations, and a growing pile of edge cases around deletes and column-level edits.

cr-sqlite moves that decision into the database. The README describes it as a run-time loadable extension for SQLite and libSQL that "allows merging different SQLite databases together that have taken independent writes." The framing is deliberately plain: you write offline, another peer writes offline, both come online and merge without conflict.

The intended audience is application developers, not database engineers. The README lists five situations where this matters: syncing data between devices, realtime collaboration, offline editing, resilience to network conditions, and instantaneous interactions. All five are the same underlying problem, and the README's argument is that if the database handles it, your application does not need custom code for any of them. The examples point the same way: a Vite starter, a TodoMVC, a Svelte store, and a work-in-progress local-first presentation editor.

## CRRs, changesets and the crsql_changes virtual table

The mechanism has two halves. The first is table conversion. cr-sqlite does not replace SQLite's storage engine; it upgrades existing tables in place with a function extension called crsql_as_crr, producing what the project calls conflict free replicated relations, or CRRs. After a table is a CRR, you insert, update and select against it as normal. The extension maintains extra bookkeeping columns behind the scenes.

The second half is the transport surface. A virtual table named crsql_changes exposes the recorded changes as rows with columns table, pk, cid, val, col_version, db_version, site_id, cl and seq. A peer selects the changes it has not seen, ships those rows to another peer, and the other peer inserts them back into its own crsql_changes. That is the whole sync protocol at the database level: select rows out, insert rows in. The README's example query filters on db_version and site_id, and a second form excludes changes that originated from a particular actor, which is how you avoid echoing a peer's own edits back to it.

Column-level granularity is worth noting. The cid field names the column that changed, and col_version tracks that column independently, so two peers editing different columns of the same row do not have to be treated as a conflict. That is the CRDT part, and it is why the project can promise merging rather than a conflict-resolution UI.

Schema changes need their own dance. Because a CRR carries extra structure, ALTER TABLE cannot be applied directly. cr-sqlite exposes crsql_begin_alter and crsql_commit_alter to bracket the alterations. The README shows the pattern and notes that a future version may extend SQL syntax to make it more natural. That note is honest, and it is also a signal: this is a young interface.

## Installing cr-sqlite and running a first merge

The repository's top-level Makefile is the build entry point. It declares a git submodule dependency under core/rs/sqlite-rs-embedded, exports CRSQLITE_NOPREBUILD=1, and the default target is crsqlite. The submodule rule runs git submodule update --init --recursive, then the build descends into core and runs make loadable, which is what produces the loadable extension artifact.

```bash
make all
```

That target is the documented path from a fresh clone to a loadable extension. The README does not spell out per-language installation beyond the loadable-extension model, so plan on loading the built artifact into whichever SQLite binding you use.

Once loaded, the README's usage example is the shortest complete demonstration. It loads the extension, creates two ordinary tables, converts both to CRRs, writes rows, and then reads the change log back out of crsql_changes.

```sql
.load crsqlite
.mode qbox
create table foo (a primary key not null, b);
create table baz (a primary key not null, b, c, d);
select crsql_as_crr('foo');
select crsql_as_crr('baz');
insert into foo (a,b) values (1,2);
insert into baz (a,b,c,d) values ('a', 'woo', 'doo', 'daa');
select "table", "pk", "cid", "val", "col_version", "db_version", "site_id", "cl", "seq" from crsql_changes;
```

What you should see is one row per changed column, not one row per statement. The README's sample output shows four rows for those two inserts: one for foo.b, and three for baz.b, baz.c and baz.d, each with its own cid and a seq value counting up within the transaction. The pk values come back as opaque blobs rather than the literal 1 or 'a' you inserted.

To sync, you select from crsql_changes on one peer, ship the rows to another, and insert them into the peer's crsql_changes virtual table. The README gives both the local-changes query and the query that excludes changes already synced from a given site, and shows the insert side as INSERT INTO crsql_changes VALUES with the patches received from the other peer.

## What you give up: constraints, schema changes and insert speed

The performance note is stated plainly in the README: inserts into CRRs are currently about 2.5x slower than inserts into regular SQLite tables, while reads are the same speed. That is the price of the bookkeeping. It is a fine trade for a notes app syncing a few rows at a time, and a poor one for a write-heavy ingest pipeline that happens to want replication as an afterthought. If most of your tables are bulk-loaded and only a few need merging, convert the few.

Schema evolution is the second constraint. Because CRRs are altered through crsql_begin_alter and crsql_commit_alter rather than plain ALTER TABLE statements, any migration tooling that emits raw DDL will need to be aware of which tables are CRRs. The README acknowledges this is awkward and says a future version may extend SQL syntax to improve it. Until then, treat migrations as a place where cr-sqlite changes your workflow.

The third is the one the README does not discuss, and it is the one that bites hardest in practice: relational invariants. A CRR is a per-row, per-column merge structure. Two peers can each satisfy a foreign key or a uniqueness rule locally and still produce a merged database that violates it, because the merge is applied column by column rather than as a transaction against your constraints. If your correctness argument depends on cross-row or cross-table invariants holding after sync, cr-sqlite is the wrong layer for that argument, and you would need to enforce it above the database. The documentation's silence on this point is a gap, not a guarantee.

## cr-sqlite compared with ElectricSQL and RxDB

The closest comparison in the same ecosystem is ElectricSQL, which also appears among the project's sponsors. Both aim at local-first applications over Postgres or SQLite, but the split is architectural: cr-sqlite is a loadable extension that lives inside your SQLite process and exposes merge state through a virtual table you query yourself, while ElectricSQL's model puts a sync service between your local database and a central Postgres. With cr-sqlite you own the transport and decide which peers talk to which; with ElectricSQL the sync layer is the product.

On the JavaScript side, RxDB is a database written in JavaScript with replication built into its own storage and query layer. It does not extend SQLite; it replaces it. That means you get a document-oriented API and a plugin ecosystem rather than SQL, and you lose the ability to drop the same merge semantics into an existing SQLite file that already has data and schema you care about. cr-sqlite's pitch is precisely that you keep your tables and your SQL.

A third option, and the one most teams actually pick, is application-level sync: an outbox table, a version counter, and conflict rules written by hand. It is more code and more bugs, but it imposes no constraints on insert throughput or migration tooling. cr-sqlite is worth adopting when that hand-written layer has become the hard part of your product.

## Maintenance status and what the MIT licence leaves you to decide

The repository is not archived, and the last push was on 2026-08-10. The most recent tagged release listed is v0.16.3 from 2024-01-17, so the release tags lag well behind the branch activity. The README carries a note that a future version may extend SQL syntax for CRR alteration, and the examples list includes a work-in-progress editor, which together suggest the interfaces are still moving.

cr-sqlite is MIT licensed, which is permissive and places few obligations on how you redistribute it. That is a statement about the licence text, not advice about your situation; the extension is loaded into your process and your build pipeline, and whether that interacts with your own distribution model is a question for your own review.

Upgrade cost is the practical concern. Because converted tables carry extension-maintained state, and because schema changes to those tables go through crsql_begin_alter and crsql_commit_alter, a version bump is not a drop-in binary swap. Verify that the new extension can open databases written by the old one before you roll it out to peers that may be offline for long stretches.

## Conclusion

Adopt cr-sqlite when your application already speaks SQL and you want offline writes merged by the database rather than by hand-rolled reconciliation code. Do not adopt it if you need cross-row or cross-table invariants such as foreign keys and aggregate constraints to survive a merge, or if you cannot accept roughly 2.5x slower inserts into converted tables. Before committing, verify which of your tables can pass crsql_as_crr, confirm your application can route all writes to CRR tables through the extension, and check that the crsql_changes shape matches what your transport expects.

## FAQ

### What is cr-sqlite used for?

It adds multi-master replication and partition tolerance to SQLite so databases that took independent writes can be merged. The README lists device-to-device sync, realtime collaboration, offline editing, network resilience and instantaneous interactions as the cases it targets.

### How do I install and load cr-sqlite?

Build it from the repository with make all, which initializes the submodule under core/rs/sqlite-rs-embedded and runs make loadable inside core. The result is a loadable extension you bring into SQLite with .load crsqlite, as the README's usage example shows.

### How do two cr-sqlite databases sync with each other?

One peer selects rows from the crsql_changes virtual table, filtered by db_version and site_id, and the other peer inserts those rows into its own crsql_changes. The README shows both the select and the insert form.

### Does converting a table to a CRR slow it down?

Yes for writes. The README states that inserts into CRRs are currently about 2.5x slower than inserts into regular SQLite tables, while reads run at the same speed.

### Can I run ALTER TABLE on a table that cr-sqlite manages?

Not directly. The README shows alterations bracketed by crsql_begin_alter and crsql_commit_alter, and notes that a future version may extend SQL syntax to make this more natural.

## Sources

- [License: MIT](https://github.com/vlcn-io/cr-sqlite/blob/main/LICENSE)
- [Project website](https://vlcn.io)
- [README](https://github.com/vlcn-io/cr-sqlite/blob/main/README.md)
- [Releases](https://github.com/vlcn-io/cr-sqlite/releases)
- [vlcn-io/cr-sqlite on GitHub](https://github.com/vlcn-io/cr-sqlite)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/vlcn-io-cr-sqlite
