A small, self-hostable sync server for end-to-end-encrypted data.
rensync-server stores opaque encrypted blobs and tells clients what changed.
Encryption keys and plaintext stay on client devices: the server does not
encrypt, decrypt, or interpret your data.
It is designed as the sync backend for Ren applications, but the HTTP API is deliberately small and can be implemented by other clients.
Each account has collections containing encrypted items. Every mutation gets a monotonically increasing sequence number. Clients keep their last seen sequence and request newer changes, making incremental sync a simple cursor operation.
- Writes use optimistic concurrency. A stale write receives the current encrypted row so the client can resolve the conflict.
- Deletes become tombstones, allowing offline clients to observe them later.
- Batch writes execute in one SQLite transaction.
- Accounts are provisioned by the server operator; there is no public signup endpoint.
The server stores:
- usernames and SHA-256 hashes of bearer tokens;
- opaque collection metadata and item blobs;
- collection and item identifiers, sizes, and sequence numbers.
The server can observe account names, IP addresses, item counts, blob sizes, request timing, and which rows change. It cannot see encryption keys or the plaintext inside blobs. Clients are responsible for using authenticated encryption correctly.
Build and start the server:
docker build -t rensync-server .
docker volume create rensync-data
docker run -d \
--name rensync \
--restart unless-stopped \
-p 127.0.0.1:8080:8080 \
-v rensync-data:/data \
rensync-serverCreate an account:
docker exec rensync /rensync user add aliceThe command prints a bearer token once. Save it securely; only its hash is stored by the server.
Check the server:
curl http://127.0.0.1:8080/v1/healthThe container serves plain HTTP. Put it behind a TLS reverse proxy before exposing it to the internet. For example, with Caddy:
sync.example.com {
reverse_proxy 127.0.0.1:8080
}| Variable | Default | Description |
|---|---|---|
RENSYNC_DATA_DIR |
/data |
Directory containing the SQLite database |
RENSYNC_LISTEN |
:8080 |
HTTP listen address |
RUST_LOG |
server defaults | Logging filter |
The SQLite database is stored at
$RENSYNC_DATA_DIR/rensync.sqlite3. Back up the data volume regularly.
rensync user add <name>
rensync user list
rensync user rm <name>
rensync serveUse --data-dir or RENSYNC_DATA_DIR with every command when running outside
the container.
Authenticated endpoints require:
Authorization: Bearer <token>| Method | Path | Purpose |
|---|---|---|
GET |
/v1/health |
Liveness check; no authentication required |
GET |
/v1/collections |
List collections |
POST |
/v1/collections |
Create, update, or delete a collection |
GET |
/v1/changes?since=<seq> |
Read changes after a cursor |
POST |
/v1/batch |
Apply item mutations atomically |
Binary metadata and blobs are represented as standard base64 in JSON. IDs are client-generated opaque strings of at most 64 bytes.
Current limits:
- 256 KiB per blob;
- 1,000 mutations per batch;
- 8 MiB of decoded blob data per batch;
- 1,000 rows per changes page.
Requires a recent stable Rust toolchain:
cargo test
cargo run -- --data-dir /tmp/rensync user add alice
cargo run -- --data-dir /tmp/rensync serve --listen 127.0.0.1:8080The runtime image is built from scratch; SQLite is statically linked into the
server binary.