Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Set up a project

A system kept in sync by magic_sync is at least two programs: the server, on PostgreSQL, and a replica, on SQLite, that runs on every device. They share one thing — the tables that travel, as Rust structs — and almost nothing else. The shop is laid out so:

shop/
├── rows/       the tables, as both sides hold them: a library
├── server/     the head office: a program, and a workspace of its own
└── register/   a register: a program, and a workspace of its own

A build of magic_sync is one side: the postgres feature builds the server’s, the sqlite feature a replica’s. A build with both is refused — the crate does not compile — and so is a build with neither. Everything in this chapter follows from that.

The rows: one crate, no side of its own

You write a library for the rows and their codecs, which depends on magic_sync without picking a side, and lets each program pick one through a feature of its own:

[package]
name = "shop_rows"
version = "0.0.0"
edition = "2021"
publish = false

[dependencies]
# No side of its own: each program that uses the rows picks one, below.
# Outside this repository, leave `path` out.
magic_sync = { version = "0.1.1", path = "../../../magic_sync" }
chrono = { version = "0.4", features = ["serde"] }
serde = { version = "1", features = ["derive"] }
sqlx = { version = "0.8", features = ["runtime-tokio", "uuid", "chrono"] }
uuid = { version = "1", features = ["serde", "v7"] }

[features]
# The server builds the rows with `server`, a register with `register`: what
# only one side has (where the registers are) is behind its feature.
server = ["magic_sync/postgres"]
register = ["magic_sync/sqlite"]

What only one side has is behind its feature: where the server finds the replicas (magic_sync::plan::Replicas) exists only on the server, so the shop’s REGISTERS is #[cfg(feature = "server")]. The rows themselves and the codec list compile on either side (The rows, codecs).

The server

You write a program with the postgres side, and the rows built for it:

[package]
name = "shop_server"
version = "0.0.0"
edition = "2021"
publish = false

# A workspace of its own: a build is the server's side or a replica's, never
# both, and Cargo builds a workspace's members together.
[workspace]

[dependencies]
# The server's side. Outside this repository, leave `path` out.
magic_sync = { version = "0.1.1", path = "../../../magic_sync", features = ["postgres"] }
# The rows both sides share, built for the server.
shop_rows = { path = "../rows", features = ["server"] }
axum = "0.8"
serde = { version = "1", features = ["derive"] }
sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "uuid", "chrono"] }
tokio = { version = "1", features = ["full"] }
tower-http = { version = "0.6", features = ["fs"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
uuid = { version = "1", features = ["serde", "v7"] }

sqlx is the engine’s database layer: the pool you hand it is sqlx’s, and so are the transactions and connections it takes. Enable its postgres driver and a runtime; the engine runs on tokio.

A replica

You write a program with the sqlite side:

[package]
name = "shop_register"
version = "0.0.0"
edition = "2021"
publish = false

# A workspace of its own: a build is the server's side or a replica's, never
# both, and Cargo builds a workspace's members together.
[workspace]

[dependencies]
# A replica's side. Outside this repository, leave `path` out.
magic_sync = { version = "0.1.1", path = "../../../magic_sync", features = ["sqlite"] }
# The rows both sides share, built for a replica.
shop_rows = { path = "../rows", features = ["register"] }
axum = "0.8"
chrono = "0.4"
reqwest = { version = "0.12", default-features = false, features = ["json"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite", "uuid", "chrono"] }
tokio = { version = "1", features = ["full"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
uuid = { version = "1", features = ["serde", "v7"] }

One workspace per side

Cargo builds the members of a workspace together and turns on, for a crate they share, every feature any of them asks for. Put the server and a replica in one workspace and the first cargo build, cargo test or editor check that builds both builds magic_sync with both sides, and the crate refuses to compile, saying so.

So each program is a workspace of its own: an empty [workspace] table in its manifest, as above, or one workspace for everything on the server and another for everything on the replica. The rows are a path dependency of both, and belong to neither. Each side’s tests run in its own workspace, and a test never needs the other side’s build: the server’s tests drive the server, a replica’s tests drive the replica.

A replica that is not written in Rust needs none of this: it follows the replica protocol, and only the server uses the crate.

Outside this repository

The shop reaches the engine by path, because it lives next to it; a project of yours depends on the published crates, and leaves path out:

magic_sync = { version = "0.1.0", features = ["postgres"] }

The rest of this part writes what goes in these crates: the tables, the rows, the codecs, then each side.