Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
110ea77
server-remake: decisions doc and black-box conformance suite (scratch…
kavinsood Oct 5, 2026
35ec3f4
Delete Node self-host: packages/cli, packages/server-node, Docker
kavinsood Oct 6, 2026
49d80e5
server-rewrite: fold flags into decisions; revoke-gate and flag tests
kavinsood Oct 6, 2026
3d04b0f
server-rewrite: D8b restore approved (operator-only, cold path)
kavinsood Oct 6, 2026
040c7fb
server-rewrite: decisions approved; status header
kavinsood Oct 6, 2026
21e279c
conformance: local legacy run matches scratch-3; suite fixes
kavinsood Oct 6, 2026
c8e90ac
server-rewrite P1: streams core behind injected ports
kavinsood Oct 6, 2026
e105da2
server: Worker router, vault and config DOs, T-HOTPATH harness (P1 ch…
kavinsood Oct 6, 2026
67e6af8
server: delete the legacy server; move client-needed shared modules t…
kavinsood Oct 6, 2026
9350f12
README: Hosting: Cloudflare only; fresh deploy, no migration (P1 chec…
kavinsood Oct 6, 2026
a76fe0b
server-rewrite: record P1 gap calls G1-G12
kavinsood Oct 6, 2026
ceaccf5
server: operator console (GET /) and mobile setup page (P4)
kavinsood Oct 6, 2026
2bb319b
README: one operator, vaults, paired devices, optional R2, restore (P4)
kavinsood Oct 6, 2026
a58f271
P2 identity: claim, operator sessions, vaults, D3 pairing and enroll,…
kavinsood Oct 6, 2026
351ef71
P2 tests: identity (enroll, replay, T-PAIR-NOWRITE, tickets, D7 revok…
kavinsood Oct 6, 2026
e539db4
server: the entry module exports only the fetch handler and the DO cl…
kavinsood Oct 6, 2026
760ec97
conformance: same-origin Origin on operator writes; T-ENROLL-200 chec…
kavinsood Oct 6, 2026
74beea7
server: serve the P4 console and mobile setup pages from the router
kavinsood Oct 6, 2026
84eee59
server-rewrite: record P2/P4 gap calls G13-G27 and O9; D6 principalId…
kavinsood Oct 6, 2026
85c037a
P3a interface: deleteAllStreamRows, discardAll, writesThisRuntime, H3…
kavinsood Oct 6, 2026
0227799
P3a streams hardening: H1 strict codec, H2 4 MiB dedupe window, H3 re…
kavinsood Oct 6, 2026
e38c915
streams: H2 CPU fallback, chunked dedupe builds and gated commits
kavinsood Oct 6, 2026
44c357c
P3b operator and cold path: D9 blobs, D8a reset-streams, D8b restore,…
kavinsood Oct 6, 2026
f89ccec
streams: charge the dedupe read in the build step budget; keep DELETE…
kavinsood Oct 6, 2026
0b818f8
streams: mark the H2 step trigger as a DECISIONS-GAP
kavinsood Oct 6, 2026
fa61ddd
conformance: T-DAILY drives the H3 debug route with a device bearer u…
kavinsood Oct 6, 2026
15e9bf3
server-rewrite: record P3a measurements, gap calls G28-G33 and O10
kavinsood Oct 6, 2026
8467dc5
server-rewrite: record P3b gap calls G34-G47, close O9, add O11, mark…
kavinsood Oct 6, 2026
6bd7c37
console: fit monospace inputs on narrow screens; restore-point text n…
kavinsood Oct 6, 2026
c754d59
server: split the DO migration into v3 (delete legacy) and v4 (add new)
kavinsood Oct 6, 2026
96e470c
docs: remove relay v2/v3 server docs superseded by DECISIONS.md
kavinsood Oct 6, 2026
4b0fd60
server: delete unused armAlarmUnderDailyLimit
kavinsood Oct 6, 2026
2c03df2
server: log restore failures as name and message; fix two lint errors
kavinsood Oct 6, 2026
55cfd32
server: restore to a time before the PITR history is invalid_restore_…
kavinsood Oct 6, 2026
f9ce3a1
server: "This database has no history." is invalid_restore_point; the…
kavinsood Oct 6, 2026
8ca8e6a
docs: record P5 (deploy, suite, latencies, T-RESTORE-MANUAL, CPU) and…
kavinsood Oct 6, 2026
d51c3ae
server: the console draws the setup QR; claim and owner-code drop mob…
kavinsood Oct 6, 2026
ec6aca0
docs: O12 resolved: the console draws the setup QR
kavinsood Oct 6, 2026
2764f20
tests/server: drop the four double casts guard:no-any flags in the ne…
kavinsood Oct 6, 2026
8134689
docs: O12 live on scratch-3; relay-wire claim response, minIntervalMs…
kavinsood Oct 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
27 changes: 0 additions & 27 deletions .dockerignore

This file was deleted.

4 changes: 0 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,10 @@ jobs:
- run: npm ci
- run: npm ci --prefix server
- run: npm run build
- run: npm run build:server-node
- run: npm run typecheck:tests
- run: npm run typecheck:qa
- run: npm run typecheck:cli
- run: npm run typecheck:server-node
- run: npm run guard:no-any
- run: npm run guard:production-bundles
- run: npm run guard:no-tracked-generated-artifacts
- run: npm run test:ci
- run: npm run test:docker
- run: npm --prefix server run typecheck
28 changes: 0 additions & 28 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ permissions:
contents: write
id-token: write
attestations: write
packages: write

jobs:
release:
Expand All @@ -29,7 +28,6 @@ jobs:
- run: npm run guard:no-tracked-generated-artifacts
- run: npm run build:server-release
- run: npm run test:ci
- run: npm run test:docker
- run: npm --prefix server run typecheck

# Sanity check: the tag must match manifest.json version
Expand All @@ -42,30 +40,6 @@ jobs:
exit 1
fi

- uses: docker/setup-buildx-action@v3

- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and publish server image
id: push-image
uses: docker/build-push-action@v6
with:
context: .
push: true
build-args: VERSION=${{ github.ref_name }}
tags: ghcr.io/${{ github.repository_owner }}/yaos-server:${{ github.ref_name }}

- name: Attest server image
uses: actions/attest@v4
with:
subject-name: ghcr.io/${{ github.repository_owner }}/yaos-server
subject-digest: ${{ steps.push-image.outputs.digest }}
push-to-registry: true

- name: Attest plugin release assets
uses: actions/attest@v4
with:
Expand All @@ -83,7 +57,6 @@ jobs:
gh release upload "$GITHUB_REF_NAME" \
yaos.zip \
dist/release-assets/yaos-server.zip \
dist/release-assets/update-manifest.json \
main.js \
manifest.json \
styles.css \
Expand All @@ -94,7 +67,6 @@ jobs:
--generate-notes \
yaos.zip \
dist/release-assets/yaos-server.zip \
dist/release-assets/update-manifest.json \
main.js \
manifest.json \
styles.css
Expand Down
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ server/node_modules
server/dist
server/.env
.wrangler
server/.partykit
server/.wrangler

# Agent/session artifacts
Expand Down
40 changes: 0 additions & 40 deletions Dockerfile

This file was deleted.

146 changes: 85 additions & 61 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,20 @@

**A zero-terminal, real-time sync engine for Obsidian, powered by your own Cloudflare Worker.**

YAOS synchronizes Markdown live across devices and invited people with CRDT merge
semantics. One server can host multiple vaults; each person has a vault-scoped
identity with separately revocable devices.
YAOS syncs your Obsidian vaults live across your devices, with CRDT merging. It is built for one person. Each
server has one operator, and holds any number of vaults. Each device is paired with a one-time code and can be
revoked on its own.

<img src="https://github.com/user-attachments/assets/ee937050-8a05-4d56-9c5f-3ae5003496fc" alt="YAOS syncing a note across desktop and mobile in real time" width="720" />

No terminal, `.env` file, database setup, or R2 bucket is required for Markdown sync.
No terminal, `.env` file, database setup, or R2 bucket is required for note sync.

[![License: 0-BSD](https://img.shields.io/badge/license-0--BSD-green)](LICENSE)

## How it compares

YAOS runs on infrastructure in your Cloudflare account. Markdown content is split into independently loaded CRDT bodies rather than one vault-wide content document.
YAOS runs on infrastructure in your Cloudflare account. The server only relays changes; merging happens on your
devices.

| | Conflicts | Real-time | Deployment | No terminal | Free |
|---|:---:|:---:|:---:|:---:|:---:|
Expand All @@ -35,86 +36,109 @@ If you want the official, fully managed experience, use Obsidian Sync. If you wa
</a>

1. **Deploy the server.** Click **Deploy to Cloudflare** above.
2. **Claim it.** Open the Worker URL, click **Claim**, and save the operator recovery key. Claiming provisions a Personal vault and returns a one-use owner-bootstrap code.
3. **Install YAOS.** Install the plugin from the Obsidian Marketplace.
4. **Enroll this folder.** Open the setup link or scan the QR code. Use **Add my device** for another installation you own and **Invite person** for a collaborator.

The operator key opens the server console. It is not a device credential and must not be pasted into plugin settings.

## Vault collaboration

Every vault has one owner and any number of full members. Everyone can read and
change the complete vault, including creating, moving, and deleting notes and
attachments. Only the owner manages people, recovery, security audit, vault
policy, ownership transfer, and destruction. YAOS intentionally does not offer
viewer roles, folder permissions, or per-person permission toggles.

Invitations grant complete plaintext read/write access. Removing a member stops
future admission and closes their sessions, but cannot erase files already
downloaded to their devices. See [vault collaboration](./docs/collaboration.md).

## Obsidian settings sync

Settings sync is enabled by default after enrollment and is independent of note sync. Before a pre-existing remote environment can change this folder, open **Settings → YAOS → Obsidian settings sync** and choose **Take the remote seed**, **Seed from this device**, or **Decide initial seed later**. The choice is stored for this exact enrollment, folder, device, and configuration-folder name. Use **Replace remote settings environment** only when this device should replace an existing seed.

YAOS synchronizes a closed allowlist of Obsidian JSON files, CSS snippets, community-plugin intents, theme intents, and version-matched plugin `data.json`. It does not store plugin or theme binaries; automatic package installation/removal requires separate explicit consent and is off by default. Each person's settings environment is private to their principal and synchronizes only across their own devices. Settings state is stored in the vault Durable Object's SQLite sidecar, not Yjs or R2. Unsupported servers, an off switch, or a detected settings-sync clash pause settings only; notes continue syncing. See [operations](./docs/operations.md#settings-sync-setup-and-operation) for setup, limits, and recovery behavior.

## Attachments and recovery

Markdown and explicitly promoted semantic Canvas files use Durable Object SQLite and work without R2. Add a `YAOS_BUCKET` R2 binding to synchronize images, PDFs, opaque Canvas fallback, and other non-Markdown content; enable Canvas promotion/demotion rollback; and enable asynchronous recovery points.
2. **Claim it.** Open the Worker URL and click **Generate a recovery key**.
- Save the operator recovery key in your password manager. It is shown only once, and it is the only way to
sign in to the console. It cannot be reset.
- Tick **I have saved the recovery key**, then click **Claim server**.
- Claiming creates your first vault and shows a one-time pairing code with a QR code.
3. **Install YAOS.** Install the plugin from Obsidian's Community plugins on each device.
4. **Pair a device.**
- On a phone, scan the QR code and tap **Connect Obsidian**.
- On a computer, click **Open in Obsidian**, or open YAOS settings, choose **Pair this device**, and enter the
server URL and the code.
- A code works once and expires after 15 minutes.
5. **Pair more devices.** Click **Pair a device** on the vault in the console. Or, on a device that is already paired,
open YAOS settings and choose **Pair another device**.

The operator recovery key opens the server console. It is not a device credential; never paste it into plugin
settings.

## One operator, vaults and devices

- **One operator.** The person who claims the server is its only operator, and the console at the Worker URL is
theirs. YAOS has no members, invitations or roles. Every paired device can read and change its whole vault.
- **Vaults.** One server holds any number of vaults. Create them in the console with **Create vault**. Each vault
syncs on its own.
- **Devices.** A device stays paired until you revoke it. **Devices** on a vault lists them.
- **Revoke** cuts a device off at once: its connections close, and changes it had not yet synced are dropped. To
use it again, pair it again.
- Revoking cannot erase files already on that device.
- **Reset streams** deletes the vault's synced content on the server and starts it over. You must type the vault
ID first. Devices stay paired and upload again from their files, and attachments are kept.
- **Delete vault** removes the vault, its devices and its attachments for good. You must type the vault ID first.

## Attachments (optional R2)

Notes sync through Durable Object storage and need no R2.

Images, PDFs and other non-note files sync through an R2 bucket bound as `YAOS_BUCKET`. The shipped
`server/wrangler.toml` binds a bucket named `yaos`. Without the binding:
- the server reports attachments as off;
- the console's header line says so;
- the server refuses attachment uploads;
- notes keep syncing.

Uploads are limited to 10 MiB per file. Reset and restore keep attachments; deleting a vault removes them.

<a href="https://youtu.be/Z7xCMEYfdFM">
<img src="https://img.youtube.com/vi/Z7xCMEYfdFM/maxresdefault.jpg" width="480" alt="Watch the R2 setup video" />
</a>

Recovery points can be captured in the background, browsed by path, and selectively restored. A deployment also needs the included `RecoveryJob` Durable Object binding and migration for this capability. See [operations](./docs/operations.md).

## Works with local tools
## Restore to a point in time

Obsidian vaults remain ordinary local files. Changes made by editors, scripts, Git tools, or agents enter the same reconciliation path and can synchronize across enrolled devices.
**Restore** in the console rewinds a vault's synced content to any moment in the last 30 days. It uses Cloudflare's
Durable Object point-in-time recovery.
- Device pairings stay as they are now: a restore rewinds content, never access.
- Unused pairing codes are cancelled.
- The vault gets a new epoch, which tells devices that the server content was replaced.
- Restore needs a deployed Worker. Local development (`wrangler dev`) has no point-in-time recovery, and the console
says so.

## Headless Linux client
Separately, each device can keep daily zip snapshots of its notes (YAOS settings, **Daily recovery snapshots**).

The Node 24 CLI synchronizes Markdown in a local directory without Obsidian. It enrolls as its own vault-scoped device; credentials are generated and stored outside the vault rather than copied from another installation.
## Works with local tools

```sh
npm run build:cli
Obsidian vaults remain ordinary local files. Changes made by editors, scripts, Git tools, or agents enter the same reconciliation path and can synchronize across paired devices.

YAOS_HOST=https://sync.example.workers.dev \
YAOS_PAIRING_CODE=... \
node packages/cli/dist/yaos.mjs enroll /srv/vault
## Hosting: Cloudflare only

node packages/cli/dist/yaos.mjs daemon /srv/vault
```
YAOS runs only on Cloudflare: one Worker with two Durable Object classes (a `VaultDO` per vault and one `ConfigDO` per server), plus an optional R2 bucket for attachments. There is no self-hosted Node or Docker server and no headless CLI client. The server core sits behind small storage, socket and clock ports so its tests run on Node; that is a test seam, not a hosting option.

The daemon is Linux/local-filesystem only, Markdown only, and single-process per vault. `.obsidian`, attachments, network filesystems, and rename-identity inference are intentionally outside its contract. See [operations](./docs/operations.md#headless-linux-client).
The server is an opaque relay. It never interprets payloads, checkpoints, stream names or blob contents.

## Self-hosted Docker server
Moving from a pre-rewrite server is a fresh deployment, not an upgrade. There is no data migration: the deploy's Durable Object migration deletes the old classes (`VaultSyncServer`, `ServerConfig`, `RecoveryJob`) together with their stored data, the new server starts unclaimed, and each device re-seeds it from its local files.

The production image packages `packages/server-node`, which runs the same
schema-8 control-plane, vault, settings, attachment, recovery, and deletion
owners as the Cloudflare Worker over Node 24, SQLite, WebSockets, and filesystem
object storage.
## Troubleshooting

```sh
YAOS_PUBLIC_ORIGIN=https://sync.example.com docker compose up --build -d
```
**YAOS: re-pair device.** The device was revoked in the console, or its vault was deleted. Pair it again with a new
code (**Pair this device**).

The `yaos-data` volume is the complete durable server state. Put TLS in front of the container, preserve that volume, and pin released deployments to an exact `ghcr.io/kavinsood/yaos-server:<version>` image. See [operations](./docs/operations.md#docker-deployment).
**Lost the operator recovery key.** The server stores only a hash of the key, so nobody can recover or reset it.
Paired devices keep syncing, but no one can sign in to the console.

## Troubleshooting
**Restore says the server cannot restore.** Point-in-time recovery exists only on a deployed Worker, and only for the
last 30 days. A time outside that window is refused before anything changes.

**Unauthorized or auth rejected:** The principal membership or this device credential is missing, revoked, or belongs to another vault. Re-enroll with a fresh purpose-correct code.
**Restore incomplete.** The console shows this banner when a restore was interrupted. Pairing, revoking and
resetting that vault wait until it finishes. The server finishes it on its own within about a minute, or you can
press **Restore** to resume it now.

**Recovery unavailable:** Recovery needs both `YAOS_BUCKET` and the `YAOS_RECOVERY_JOBS` binding. Markdown sync remains available without R2.
**YAOS: daily limit.** On the Workers Free plan, Durable Objects allow 100,000 rows written per day. Past that the
server refuses writes until the limit resets; the console shows the reset time.

**Cloudflare deployment issues:** See [operations](./docs/operations.md#troubleshooting), including the required Durable Object migration and R2 binding.
**Attachments do not sync.** If the console's header line says attachments are off, add the `YAOS_BUCKET` R2 binding
and redeploy. Also check **Sync attachments** and **Maximum attachment size (MB)** in YAOS settings. The server
accepts at most 10 MiB per file.

**Files not syncing:** Check exclusions and file-size limits, then use **Show sync debug info**. Safe exports redact the server URL, vault and device identity, credentials, and vault paths.
**Files not syncing.** Check **Excluded paths** in YAOS settings. Then use **Export diagnostics**, which saves a
file without note contents or credentials. If sync seems stuck, use **Rebuild local cache**.

## Engineering documentation

The compact current source set is indexed in [docs/README.md](./docs/README.md).
- [docs/server-rewrite/DECISIONS.md](./docs/server-rewrite/DECISIONS.md): the server design, including its route
table.
- [docs/client-remake/relay-wire.md](./docs/client-remake/relay-wire.md): the wire contract that design amends (see
its section 5).

## License

Expand Down
Loading
Loading