If you're moving SQLite, smuggl it.
LAN UDP fan-out. S3 relay. Host-to-host. D1, Turso, rqlite, Datasette, StarbaseDB, SQLite Cloud. One binary.
Nothing else combines these shapes in a single tool. Litestream does S3 backup. rqlite does Raft. ElectricSQL needs a server. smugglr does host, LAN, relay, and hosted backend: content-hashed delta sync, no coordinator, no service to run.
Installs the latest checksum-verified binary on macOS and Linux. On Windows, use cargo install smugglr or the release binary.
scope
# one paragraphsmugglr is a SQLite sync engine. Single Rust binary. Moves rows between local files, hosted SQLite backends (D1, Turso, rqlite, Datasette, StarbaseDB, SQLite Cloud), peers on the same LAN via encrypted UDP multicast, peers across the network via direct host-to-host, and peers across the internet via S3 relay. Content-hashed delta sync. No coordinator service. --output json on every command. Typed exit codes on every command.
install
# same algorithm · same adapters · same verbs| Channel | Command | Artifact |
|---|---|---|
| Native CLI | cargo install smugglr | Rust binary, also pre-built for macOS/Linux/Windows |
| Rust crate | cargo add smugglr-core | Embeddable engine, no CLI deps |
| npm (WASM) | npm install smugglr | Browser + Node WASM build |
| Script | curl -fsSL https://smugglr.dev/install | bash | Checksum-verified binary installer (macOS + Linux) |
targets
# one HTTP SQL plugin · per-target profiles| Target | Profile | Protocol | Auth | Notes |
|---|---|---|---|---|
| Local SQLite | local | Filesystem | n/a | rusqlite direct access |
| Cloudflare D1 | d1 | HTTP REST | Bearer token | account_id + database |
| Turso / libSQL | turso | Hrana (HTTP + WS) | JWT | sqld-compatible |
| rqlite | rqlite | HTTP REST | Basic / mTLS | Self-hosted Raft cluster |
| Datasette | datasette | HTTP JSON | Plugin-based | Read-only by default |
| StarbaseDB | starbasedb | HTTPS + WebSocket | API key | Edge (CF Workers + DO) |
| SQLite Cloud | sqlite-cloud | HTTP REST (Weblite) | API key | Managed cluster |
shapes
# one engine · five topologies| Shape | Commands | Topology | When to use |
|---|---|---|---|
| Direct sync | push · pull · sync · diff | Host sees target directly | Default case, everyday use |
| Watch daemon | watch | Continuous direct sync | Background sync, JSONL per tick |
| S3 relay | stash · retrieve | Host → S3 → Host | Machines cannot see each other |
| LAN broadcast | broadcast | Masterless UDP multicast on LAN | Same subnet. Every node broadcasts and listens. Key possession = membership. Content-hash idempotent. Heartbeat reconciles late joiners. |
| Snapshot/restore | snapshot · snapshots · restore | Point-in-time dump to relay | Portable across targets |
LAN broadcast is a masterless, controllerless mesh. No coordinator. No leader. No central node. Every peer broadcasts and listens on equal footing. Key possession is the membership check. Nothing else in this space works this way.
- Masterless.
- No primary, no secondary, no coordinator, no leader election. Every node is equal.
- Peer-symmetric.
- Every node is both a broadcaster and a listener. No client/server distinction.
- Membership = key possession.
- The shared encryption key IS the access control. No auth handshake, no identity management, no certificates. Have the key, you are on the network. Key rotation is how a machine leaves.
- Idempotent apply.
- Incoming row compared against local content hash: new or different → upsert, identical → skip. Applying the same row twice is a no-op, so lost packets are safe.
- Last-received-wins on divergence.
- If two peers hold the same primary key with different contents, the receiver replaces local with incoming. UUIDv7 primary keys make concurrent divergent writes on the same logical entity rare in practice: time-ordered IDs mean two nodes creating "the same thing" at the same instant produce different rows, not a merge conflict. No vector clocks, no CRDTs.
- Heartbeat reconciliation.
- Every node periodically broadcasts a primary_key → content_hash map for all rows it holds. Peers compare against local state and pull any missing or differing rows. Covers late joiners, missed packets, and rejoining after a partition.
commands
# typed exit codes on every command| Command | Purpose | Output | Exit |
|---|---|---|---|
| push | Send local rows to target | text / json | 0 1 2 3 4 |
| pull | Fetch target rows to local | text / json | 0 1 2 3 4 |
| sync | Bidirectional push + pull | text / json | 0 1 2 3 4 |
| diff | Report differences, write nothing | text / json | 0 1 2 |
| watch | Continuous sync on interval | JSONL per tick | 0 1 2 3 |
| stash | Upload diff to S3 relay | text / json | 0 1 2 3 |
| retrieve | Pull stashed diff from S3 | text / json | 0 1 2 3 5 |
| broadcast | Encrypted UDP sync on LAN | text / json | 0 1 2 3 |
| snapshot | Point-in-time dump to relay | text / json | 0 1 2 3 |
| snapshots | List available snapshots | text / json | 0 1 2 3 |
| restore | Restore from snapshot | text / json | 0 1 2 3 5 |
| status | Report current config + state | text / json | 0 2 |
exit codes
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | General error |
| 2 | Config error |
| 3 | Network / transient, retry suggested |
| 4 | Conflict, resolution required |
| 5 | Not found (snapshot, retrieve key, etc.) |
output contract
| Flag | Behaviour |
|---|---|
| --output text | Human-readable, columnar (default) |
| --output json | Structured, stable schema, agent-parseable |
| --verbose, -v | Detailed progress to stderr |
| --dry-run | push / pull / sync: reads, diffs, reports, writes nothing |
| exit codes | Typed. Agents can branch without parsing output. |
constraints
# what smugglr refuses to dovs alternatives
# specific, falsifiable comparisons| Tool | Covers | Does not cover |
|---|---|---|
| Litestream | SQLite → S3 continuous backup | Peer-to-peer sync, LAN, hosted backends, pull |
| rqlite | Raft-clustered SQLite (its own cluster) | Sync to external targets, cross-vendor portability |
| ElectricSQL | Postgres ↔ SQLite with CRDT sync | Needs sync service |
| PowerSync | Backend-DB ↔ SQLite managed sync | OSS, self-hosted simplicity, LAN |
| Turso embedded | Turso-to-local replicas | Any non-Turso target |
| syncthing / rsync | Filesystem sync | Row awareness, content-hashed delta, SQL writes |
| smugglr | Host-to-host, LAN UDP, S3 relay, hosted SQLite backends, one binary | Transforms (paid), non-SQLite databases |
Full comparison at /compare/. Building a multiplayer game? /compare/games/ covers Photon, Colyseus, Mirror, and the persistent tier behind a netcode framework.
A TOML file, a dry-run, a push.
The whole product in thirty seconds. Real CLI output, no mockups.
[source]
path = "./local.db"
[target]
type = "d1"
database = "my-db"
account_id = "abc123"
api_token = "..."
$ smugglr push --dry-run
Comparing source and target...
abilities 47 new, 0 changed, 0 missing
missions 3 new, 1 changed, 0 missing
characters 0 new, 0 changed, 0 missing
51 rows to write. 0 conflicts. No data moved.
$ smugglr push
abilities 47 inserted
missions 3 inserted, 1 updated
characters no change
51 rows written in 2.3s. exit 0.
The dry-run writes nothing. The push writes exactly what the dry-run described. Same output, same counts. No surprises.
Eight profiles ship today. A backend that fits an existing request shape is config, not a fork.
Every remote target smugglr reaches over HTTP goes through one plugin, smugglr-http-sql. Pick a built-in profile by name and swapping targets is a config change, not a recompile.
d1 # Cloudflare D1
turso # Turso / libSQL
rqlite # rqlite
datasette # Datasette
starbasedb # StarbaseDB
sqlite-cloud # SQLite Cloud
http-sql # http-sql v0.1 wire spec
generic # flat {sql, params} JSON
This target points at D1:
[target]
type = "d1"
database = "my-db"
account_id = "abc123"
api_token = "..."
This one at Turso:
[target]
type = "turso"
url = "https://my-db.turso.io"
auth_token = "..."
Same config file, same commands, same diff engine, different profile. A backend whose HTTP requests match the generic or http-sql shape works by pointing at that profile in config. A backend that speaks a genuinely new request or response shape needs a new profile in the core, which is a contribution rather than just a config edit.
The OSS tool syncs any number of LIKE sources from a single config.
Same algorithm, same adapters, same verbs.
cargo install smugglr for the native CLI, with pre-built binaries for macOS, Linux, and Windows. cargo add smugglr-core to embed the engine in a Rust service. npm install smugglr for the WASM build in Node or a modern browser.
Four entry points for the 10% reading this as a human.
Two reads of the same comparison.
Honest, falsifiable, no marketing. Where smugglr fits next to Litestream, rqlite, ElectricSQL, PowerSync, Firebase, Supabase Realtime, and the rest. Game devs get a separate page that names Photon, Colyseus, Mirror, and the persistent tier behind a netcode framework.
MIT licensed. All of it.
The CLI, the core engine, the plugin system. On github.com/rafters-studio/smugglr.