smugglr
sqlite sync engine · one rust binary · mit

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.

01 / 05 Direct sync push · pull · sync · diff
Shapes · 5 topologies
HOST TARGET rows HOST TARGET tick · jsonl HOST A S3 HOST B stash retrieve PEER PEER PEER PEER udp multicast · masterless D1 .snap RELAY TURSO

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.

» Machine-readable summary · for the 90%. dense · structured · decision-useful.
GET /llms.txt ↗
## 01

scope

# one paragraph

smugglr 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.

## 02

install

# same algorithm · same adapters · same verbs
ChannelCommandArtifact
Native CLIcargo install smugglrRust binary, also pre-built for macOS/Linux/Windows
Rust cratecargo add smugglr-coreEmbeddable engine, no CLI deps
npm (WASM)npm install smugglrBrowser + Node WASM build
Scriptcurl -fsSL https://smugglr.dev/install | bashChecksum-verified binary installer (macOS + Linux)
## 03

targets

# one HTTP SQL plugin · per-target profiles
TargetProfileProtocolAuthNotes
Local SQLitelocalFilesystemn/arusqlite direct access
Cloudflare D1d1HTTP RESTBearer tokenaccount_id + database
Turso / libSQLtursoHrana (HTTP + WS)JWTsqld-compatible
rqliterqliteHTTP RESTBasic / mTLSSelf-hosted Raft cluster
DatasettedatasetteHTTP JSONPlugin-basedRead-only by default
StarbaseDBstarbasedbHTTPS + WebSocketAPI keyEdge (CF Workers + DO)
SQLite Cloudsqlite-cloudHTTP REST (Weblite)API keyManaged cluster
## 04

shapes

# one engine · five topologies
ShapeCommandsTopologyWhen to use
Direct syncpush · pull · sync · diffHost sees target directlyDefault case, everyday use
Watch daemonwatchContinuous direct syncBackground sync, JSONL per tick
S3 relaystash · retrieveHost → S3 → HostMachines cannot see each other
LAN broadcastbroadcastMasterless UDP multicast on LANSame subnet. Every node broadcasts and listens. Key possession = membership. Content-hash idempotent. Heartbeat reconciles late joiners.
Snapshot/restoresnapshot · snapshots · restorePoint-in-time dump to relayPortable across targets
LAN broadcast semantics

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.
## 05

commands

# typed exit codes on every command
CommandPurposeOutputExit
pushSend local rows to targettext / json0 1 2 3 4
pullFetch target rows to localtext / json0 1 2 3 4
syncBidirectional push + pulltext / json0 1 2 3 4
diffReport differences, write nothingtext / json0 1 2
watchContinuous sync on intervalJSONL per tick0 1 2 3
stashUpload diff to S3 relaytext / json0 1 2 3
retrievePull stashed diff from S3text / json0 1 2 3 5
broadcastEncrypted UDP sync on LANtext / json0 1 2 3
snapshotPoint-in-time dump to relaytext / json0 1 2 3
snapshotsList available snapshotstext / json0 1 2 3
restoreRestore from snapshottext / json0 1 2 3 5
statusReport current config + statetext / json0 2
## 06

exit codes

CodeMeaning
0Success
1General error
2Config error
3Network / transient, retry suggested
4Conflict, resolution required
5Not found (snapshot, retrieve key, etc.)
## 07

output contract

FlagBehaviour
--output textHuman-readable, columnar (default)
--output jsonStructured, stable schema, agent-parseable
--verbose, -vDetailed progress to stderr
--dry-runpush / pull / sync: reads, diffs, reports, writes nothing
exit codesTyped. Agents can branch without parsing output.
## 08

constraints

# what smugglr refuses to do
× No WAL
smugglr does not set journal_mode. Safe for sole-writer; unsafe as a default.
× No state in user database
Cursors, config, PID live in smugglr's own config space.
× No server
smugglr does not run a daemon you connect to. Watch is a local process on an interval.
## 09

vs alternatives

# specific, falsifiable comparisons
ToolCoversDoes not cover
LitestreamSQLite → S3 continuous backupPeer-to-peer sync, LAN, hosted backends, pull
rqliteRaft-clustered SQLite (its own cluster)Sync to external targets, cross-vendor portability
ElectricSQLPostgres ↔ SQLite with CRDT syncNeeds sync service
PowerSyncBackend-DB ↔ SQLite managed syncOSS, self-hosted simplicity, LAN
Turso embeddedTurso-to-local replicasAny non-Turso target
syncthing / rsyncFilesystem syncRow awareness, content-hashed delta, SQL writes
smugglrHost-to-host, LAN UDP, S3 relay, hosted SQLite backends, one binaryTransforms (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.

01 · show it running

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.

02 · one plugin, any LIKE backend

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.

03 · how it ships

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.

04 · learn

Four entry points for the 10% reading this as a human.

05 · compare

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.

06 · open source

MIT licensed. All of it.

The CLI, the core engine, the plugin system. On github.com/rafters-studio/smugglr.