Self-Hosting a Sync Relay
A relay is a small HTTP service that stores ciphertext and nothing else. This repository ships two interchangeable implementations of it. Both speak the same wire contract — one conformance suite runs against both — so an app can't tell them apart, and you can move between them or run both at once.
| On Cloudflare | On your own server | |
|---|---|---|
| Stack | Workers + D1 + R2 | One Go binary + SQLite |
| Runs on | Your Cloudflare account | Any Linux server, VPS, home machine, or Docker host |
| You operate | Nothing — and you get an HTTPS workers.dev URL |
The machine, HTTPS, and backups (configs included) |
| Setup | One command, or ask an agent | docker compose up -d, or a static binary |
| Pick it when | You'd rather not run a server | You'd rather not depend on any one provider |
Cloudflare comes first below because it needs the least operating; On your own server covers the Go relay.
Your relay URL does not need to be secret. SimplySync encrypts every sync event on the device before uploading it, and the relay never receives the owner's decryption key. Each owner is also protected by a private write capability derived from the recovery key.
The source in this repository remains the product boundary for self-hosting: the steps below deploy it into your Cloudflare account or onto your own server. Optional managed hosting is provided separately by SimplySync Cloud, whose site shows current plans and checkout availability.
#On Cloudflare
#Easiest setup: ask an AI agent
Open this SimplySync repository in ChatGPT/Codex or Claude Code and say:
Host my own SimplySync relay.The repository includes the same host-relay skill for both agents. It checks
and installs local tooling, opens Cloudflare signup or OAuth when needed,
creates D1 and R2, applies migrations, deploys the relay, verifies /health,
and returns a verified URL to paste into your app's Relay URL setting.
The agent pauses only when Cloudflare requires you to enter a password, MFA, CAPTCHA, billing details, or accept terms. Enter those details directly in the Cloudflare page; never paste them into the chat.
You can also invoke it directly with Use $host-relay to host my own SimplySync relay.
The agent deploys the exact checked-out source. It does not fetch a template, publish a package, or depend on a separate relay repository. You do not need a domain, VPS, Docker, port forwarding, private networking, or a TLS certificate. Cloudflare provides HTTPS for the public relay address.
#One setup command from a selective clone
curl -fsSL https://raw.githubusercontent.com/simply-hq/simplysync/main/clone-simplysync.sh |
sh -s -- relay-cloudflare simplysync-cloudflare-relay
cd simplysync-cloudflare-relay
bun install --no-save
bun run relay:cloudflare:setupThe selective clone contains only the Cloudflare relay and repository-level
scripts. The final command authenticates, creates or reuses D1 and R2, applies
migrations, deploys, and waits for /health to return {"ok":true} before it
prints the relay URL.
#Check that it works
- Require the setup command to print
Relay verifiedand the relay URL. - Paste the URL into the first device's Relay URL field and sync a change.
- Restore the same recovery key on a second device, configure the same URL, and confirm the change appears there.
- Keep any previous relay data or configuration until this test succeeds. Do not call the new relay redundant until existing encrypted history has been pushed to it and the restore test passes.
#What Cloudflare creates
| Resource | Purpose |
|---|---|
| Worker | Receives authenticated sync requests at your public relay URL. |
| D1 database | Stores encrypted events and small routing metadata. |
| R2 bucket | Stores encrypted attachment chunks. |
The default relay includes per-IP and per-owner rate limits, request-size caps,
an owner-count cap, and per-owner storage quotas. A personal deployment can use
its generated workers.dev address directly. Custom domains and additional WAF
rules are optional controls for larger shared deployments, not setup
requirements.
#Basic maintenance
- Keep the Cloudflare account accessible and enable usage notifications if you want alerts.
- Cloudflare runs log compaction automatically every six hours.
- If the relay is unavailable, the app keeps working locally and resumes sync when it returns.
- Removing a relay URL from the app does not delete local app data.
For implementation details and advanced controls, see the
relay-cloudflare README.
#On your own server
The Go relay (apps/relay-go) is one static binary over SQLite: no runtime to
install, no node_modules, and little memory, so it runs on anything from a small
VPS to a spare machine at home. It serves exactly the endpoints the Cloudflare
relay does, so apps need no changes — point them at its URL.
#With Docker
curl -fsSL https://raw.githubusercontent.com/simply-hq/simplysync/main/clone-simplysync.sh |
sh -s -- relay-go simplysync-relay
cd simplysync-relay/apps/relay-go
docker compose up -d # builds the image, serves port 4100
curl -s http://localhost:4100/health # {"ok":true,...}All state lives in a named Docker volume, so docker compose down and image
upgrades keep it.
#Without Docker
cd simplysync-relay/apps/relay-go
make build-linux # a static binary for any distro; build-linux-arm64 for ARMThis needs Go installed; the exact version the relay pins is fetched on first
build. To run it as a service,
deploy/DEPLOY.md walks through systemd,
TLS, and backups step by step.
#Give it an HTTPS URL
Port 4100 speaks plain HTTP, and apps need HTTPS, so put a reverse proxy that
terminates TLS in front of it. The included
deploy/Caddyfile does this with an
automatic certificate — replace its domain with yours. Then set
TRUST_PROXY_HEADERS=1 so rate limits see each client's real address, and keep
port 4100 reachable only from the proxy: with that flag on, anyone who reaches
the relay directly can forge their IP.
Then run the same end-to-end test as for Cloudflare: steps 2–4 of Check that it works.
#Configuration
Everything is an environment variable, and the defaults are sensible. The ones you are most likely to change:
| Variable | Default | What it does |
|---|---|---|
PORT |
4100 |
Listen port. |
RELAY_DATA_DIR |
data |
Where the SQLite database lives. |
MAX_OWNER_BYTES |
1 GiB | Storage quota per owner. |
MAX_TOTAL_BYTES |
off | Ceiling across all owners. Set it to the disk size minus headroom, so a few full owners can't fill the volume. |
ADMIN_TOKEN |
unset | Enables the ops dashboard at a secret path, printed to the logs. |
TRUST_PROXY_HEADERS |
off | Trust the proxy's forwarded client IP. Only behind a proxy that overwrites it. |
The full list is in the relay-go README.
#Backups
Losing the relay's database isn't fatal: every device keeps the whole database
and uploads it again to an empty relay. Blob chunks are the exception — they
live only on the relay (see below) — so back the database up if your app stores
images or files. The deploy kit streams it continuously with
Litestream to any S3-compatible bucket; the example
config targets Cloudflare R2, but AWS S3, Backblaze B2, or MinIO work the same
way. deploy/DEPLOY.md includes a
restore drill.
#Moving existing data to your relay
Your data, identity, and encryption keys derive from your recovery phrase; they are not tied to a particular relay — or to a provider. Nothing needs exporting from the old relay: every device keeps the whole database, and the engine uploads a full copy of the current rows to any relay it hasn't synced with before. You can list both relays while you move — each one receives every change — and remove the old one once the new one checks out.
- Configure the new relay on the device that has the complete local history.
- Sync so that history is pushed to the new relay.
- Restore the same recovery key on another device, configure the new URL, and confirm the historical and new rows both appear.
- Retain the old relay data/configuration until this test succeeds.
Blob chunks don't move with the rows. Images and other blobs are stored only on the relay that received them, and the engine keeps no local copy, so moving relays carries their manifests across but not their bytes. Re-upload them from wherever your app keeps the originals before you retire the old relay.
#Security model
The relay stores only:
- encrypted event ciphertext,
- a pseudonymous owner id,
- a hash of the owner's write capability, and
- encrypted attachment chunks.
A malicious or compromised relay can refuse to sync or delete stored events, but it cannot read or forge your app data. Keep an encrypted backup for disaster recovery. See Sync & the relay and the security model for the protocol details.