yaos sync server
Live Obsidian sync from a server written for Cloudflare Workers, run unmodified on the devbox.
Names and addresses are placeholders. The list says what each one stands for.
What it does here
yaos keeps an Obsidian vault in sync between my laptop and phone in real time. It's a CRDT sync plugin with a small server. The server runs on the devbox under workerd, with attachments and snapshots in Garage, and only the tailnet can reach it.
Canonical setup
Upstream deploys the server to Cloudflare Workers, with the bindings declared in server/wrangler.toml. Each device then installs the YAOS plugin and claims the server. Upstream: kavinsood/yaos.
The server binds these:
- A Worker with HTTP and WebSocket routes: claim, tickets, the sync socket, blobs and snapshots. Idle sync rooms use WebSocket hibernation.
- Durable Object
VaultSyncServer, SQLite-backed, holding each vault's Yjs doc. - Durable Object
ServerConfig, SQLite-backed, holding the claimed token hash. - Optional R2 binding
YAOS_BUCKETfor attachments and daily snapshots. Markdown sync works without it.
Alarms aren't used. A code comment says they avoided them to save free-tier writes.
In this build
- yaos itself is unmodifiedIt's fetched at a pinned commit, so an upstream release never means rebasing a patch.
- Durable Objects on workerdBoth run SQLite-backed on local disk. See the workerd card.
- `YAOS_BUCKET` is a shim over Garageyaos calls only
get,put,head,listanddelete, all insnapshot.tsandroutes/blobs.ts. See the R2 shim card. - Claimed from a tailnet device before anyone gets the URLThe name only resolves to a tailnet address, so nothing off the tailnet can open the claim page.
yaos.dev.tailnet.example {
tls {
dns cloudflare {env.CF_API_TOKEN}
}
reverse_proxy 127.0.0.1:8787
}Claiming generates the token and an obsidian:// setup link with a QR code. Each device installs the YAOS plugin and opens the link.
To update, change YAOS_REF in the Dockerfile and rebuild. The plugin's "update server" button rewrites a Cloudflare deploy repo and does nothing here. The plugin and server have to agree on the schema version, so read the release notes first.
Uploads are capped at 10 MB each. That's yaos's default, not something the shim adds.
Seams
Where this meets the rest of the build. Each one leads with what you will actually see.
Symptomthe first person to open the page owns the server
A fresh yaos server is unclaimed, and whoever opens the claim page first owns it. On a public URL that's a race against scanners. Keep the name on the tailnet and claim it from a tailnet device before telling anyone the URL.
Verify
yaos ships a live test suite in tests/live, normally run against wrangler dev. Each test reads its host from YAOS_TEST_HOST. Claim the server with a throwaway token, point the suite at it, then wipe the data and run setup.sh again.
YAOS_TEST_HOST=https://yaos.dev.tailnet.example
# schema-guard, provider-manual-connect, sync-client (x2), snapshots,
# hardening-worker, ws-ticket-reconnect, ws-admission-protocol: all passlive-seed-check in seed mode writes a note with a timestamp. After docker compose restart, verify mode should read back the same timestamp.
- 4 Nov 2026First written.