Fall 2026brianmars.comBuilt at EGBT Technologies
ReferenceSync

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.

From the manual

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_BUCKET for 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.

Edited for this build

In this build

  1. yaos itself is unmodifiedIt's fetched at a pinned commit, so an upstream release never means rebasing a patch.
  2. Durable Objects on workerdBoth run SQLite-backed on local disk. See the workerd card.
  3. `YAOS_BUCKET` is a shim over Garageyaos calls only get, put, head, list and delete, all in snapshot.ts and routes/blobs.ts. See the R2 shim card.
  4. 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.
/srv/stacks/caddy/Caddyfile
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.

Where it breaks

Seams

Where this meets the rest of the build. Each one leads with what you will actually see.

The claim page meets a public URL

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.

Live suites
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 pass

live-seed-check in seed mode writes a note with a timestamp. After docker compose restart, verify mode should read back the same timestamp.

Corrections and updates
  1. 4 Nov 2026First written.