# @doubling/compound-sync

> Bidirectional sync between Compound and local markdown files

Latest version **2.1.3** (published 2026-10-17) · 0 weekly downloads

## Install

```sh
npm install @doubling/compound-sync
pnpm add @doubling/compound-sync
yarn add @doubling/compound-sync
bun add @doubling/compound-sync
```

## Health

**Score 55/100 (C)** — status: active.

Positive: no vulnerabilities; recently updated; high maintenance score.

Warnings: low downloads; no types; no esm support.

## Facts

| | |
|---|---|
| Version | 2.1.3 |
| Published | 2026-10-17 |
| First published | 2026-04-11 |
| Weekly downloads | 0 |
| TypeScript types | none |
| Module format | CommonJS |
| Dependencies | 0 |
| Known vulnerabilities | 0 |
| Install scripts | no |
| Maintainers | engineering-doubling |

## Links

- npm: https://www.npmjs.com/package/@doubling/compound-sync
- npm.io page: https://npm.io/package/@doubling/compound-sync

## Recent versions

- 2.1.3 (latest) — 2026-10-17
- 2.1.2 — 2026-10-15
- 2.1.1 — 2026-10-09
- 2.1.0 — 2026-10-08
- 2.0.1 — 2026-09-23
- 1.16.17 — 2026-09-15
- 1.16.16 — 2026-09-14
- 1.16.15 — 2026-09-13
- 1.16.14 — 2026-09-11
- 1.16.13 — 2026-09-10
- 1.16.12 — 2026-09-09
- 1.16.11 — 2026-09-07
- 1.16.10 — 2026-09-06
- 1.16.9 — 2026-09-06
- 1.16.8 — 2026-09-01
- … 42 more at https://npm.io/package/@doubling/compound-sync/versions

## README

# Compound Sync

`@doubling/compound-sync` keeps a local folder of markdown and other
files in two-way sync with a Compound workspace, so you can edit in
any local editor or AI tool. This README is for people who install,
run or develop the daemon. How it works inside is in
[DESIGN.md](DESIGN.md); the exact CLI contract is in
[cli/README.md](cli/README.md).

## Quick start

```bash
npx @doubling/compound-sync setup --config ~/.config/compound-sync/config.json
npx @doubling/compound-sync run   --config ~/.config/compound-sync/config.json
```

`setup` opens a browser to sign in (Google or email and password),
asks which workspaces to sync and where, and writes the config. `run`
is the long-lived daemon; stop it with Ctrl+C. Requires Node.js 22 or
later.

Pass `--config` as an absolute path. A relative path resolves against
the package install directory, which under `npx` is a cache you will
not find again.

### TLS certificates

Node ignores the OS certificate store, so Google and Firebase sign-in
requests fail with `auth/network-request-failed` under Node's bundled
roots. When `NODE_EXTRA_CA_CERTS` is unset, the daemon re-execs itself
with it set to the system bundle (`/etc/ssl/cert.pem` on macOS,
`/etc/ssl/certs/ca-certificates.crt` or
`/etc/pki/tls/certs/ca-bundle.crt` on Linux). Behind a corporate proxy
with its own roots, set it yourself:

```bash
NODE_EXTRA_CA_CERTS=/path/to/corporate-bundle.pem npx @doubling/compound-sync run --config ...
```

## What it does

The daemon watches your local folder and your workspace at the same
time. A create, edit, rename or delete on either side reaches the
other. Text files merge through a CRDT, so concurrent edits in the
browser and on disk both survive.

```
{Sync Folder}/
  {TeamName} Teamspace/   team files
  Private/                your private files
  Shared by Me/           symlinks to private files you have shared
  Shared with Me/         files others have shared with you (read-only)
```

The daemon creates these top-level folders, and they keep their names.
If you rename or remove one while sync is running, the files in it are
not deleted from your workspace. The daemon logs one warning and puts
the folder back a moment later. It renames the folder back when it
can, and otherwise creates it again and downloads its files. To delete
files, delete the files or the sub-folders inside.

This covers a rename, a move to the Trash, and a delete that removes
the folder piece by piece, as `rm -r` does. There is one limit. If the
daemon sees nothing leave the folder for more than two seconds partway
through such a delete, the parts that were already removed are deleted
from your workspace. That happens when the delete stalls. It can also
happen with a tool that deletes in an unusual order, while it is busy
in a dot-directory or `node_modules`. The warning then says how many
deletes were sent before the folder went.

Because of this check, deletes that you make one after another inside
one of these folders reach your workspace together, two seconds after
the last one.

If one of these folders was renamed or removed while sync was off,
nothing is deleted either. The next start creates the folder again and
downloads its files. A folder that was renamed is left beside the new
one and is not synced.

Access follows your role on each file ([`file-role.ts`](file-role.ts),
which mirrors the server rules). A file you may only view is written
read-only on disk and never uploaded, wherever it sits.

Everything in `Shared with Me/` is read-only on disk, including a file
shared with you as an editor, because the daemon does not upload
changes made there. To change a file someone shared with you, edit it
in the web app. A file you add to the folder yourself is not uploaded
either.

If a file there is changed anyway, the daemon tells you, in its log
and as a notification in Compound.app. It never replaces your version
without keeping it:

- A note that the owner edits in the web app is kept up to date as
  they type. Your version is moved to `.compound-trash/` before the
  owner's text is written. That happens when the owner next types, and
  also the next time the daemon starts.
- Any other file, such as an image, or a text file that was never
  edited in the web app, is left in place while you are asked to
  choose between the two versions.

The daemon also keeps per-machine state in dotfiles and a recovery
folder, `.compound-trash/`, under the sync folder. A file removed from
your view by a remote change is moved there rather than deleted. So is
your version of a file in `Shared with Me/` that the owner's version
replaced. Files stay there for 30 days. Do not edit the dotfiles by
hand. The full table is in [DESIGN.md](DESIGN.md#local-metadata).

## Commands

```bash
compound-sync setup      # sign in, choose workspaces, write the config
compound-sync run        # long-lived daemon
compound-sync sync       # converge once and exit (--timeout, default 120 s)
compound-sync discover   # list your workspaces as a JSON event (use --json)
compound-sync status     # offline: config, session, last sync, daemon pid
compound-sync version
```

Common flags: `--config <file>`, `--env <name>`, `--json`, `-v`,
`--no-color`. Verbs, flags, exit codes, output streams, the stdin
control channel and every environment variable are in
[cli/README.md](cli/README.md).

### Multiple workspaces

One daemon can sync several workspaces: `setup` asks which ones (a
comma-separated list or `all`) and a folder for each. You can also
keep separate configs and run one daemon per config. Only one daemon
may run per folder; a second `run` on the same folder exits 2.

## Security

- The first sign-in is interactive. After that the daemon saves the
  Firebase refresh token at
  `~/.config/compound-sync/{project}__{account}.json` with mode
  `0600`, written atomically, so a restart does not prompt
  ([`auth-persistence.ts`](auth-persistence.ts)). The desktop app sets
  `COMPOUND_AUTH_FILE` per account.
- The config file holds only `projectId` and the org-to-folder
  mapping, no credentials.
- The daemon uses the Firebase client SDK with App Check, under the
  same Firestore and Storage rules as the web app. It has no admin
  access and sees only what the signed-in user could see in the
  browser.

Details:
[docs/security/09-sync-daemon.md](../docs/security/09-sync-daemon.md).

## Package layout

`sync/` is flat on purpose. The `exports` map's `./*` pattern makes
every top-level module a public deep-import path, so moving files into
folders would break consumers. The grouping of modules by role lives
in the [module map](DESIGN.md#module-map) instead.

| Path                                                                                                | Contents                                                                                                      |
| --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `sync.ts`                                                                                           | The `bin`: a CA re-exec shim that loads `cli/main.js`.                                                        |
| `cli/`                                                                                              | The process boundary: verbs, rendering, pid file, lease, control channel. See [cli/README.md](cli/README.md). |
| `agent.ts`, `org-sync.ts` and the other top-level `.ts` files                                       | The library: Firebase setup, the per-org engine, pure policies, local stores and the Yjs stack.               |
| `*-harness.ts`, `test-*.{ts,mjs}`, `scale-*.ts`, `events.fixtures.ts`, `yjs-memory-update-store.ts` | Test infrastructure. Not in the package `files`, so not published.                                            |

## Development

Run from source with the `tsx` loader; no build is needed:

```bash
npm run sync -- setup --env sandbox --config "$PWD/config-sandbox.json"
npm run sync -- run   --env sandbox --config "$PWD/config-sandbox.json"
npm run sync -- run   --env emulator --config "$PWD/config-emulator.json"
```

`config-*.json` files are gitignored. `--env` also accepts a personal
sandbox id, `compound-sandbox-<slug>`, together with
`COMPOUND_FIREBASE_CONFIG`; see [cli/README.md](cli/README.md#--env).

### TypeScript

- Each `.ts` file compiles to a sibling `.js`, `.d.ts` and source map
  (`outDir: "."`). The emitted files are gitignored build output;
  never edit them.
- Imports use the `.js` extension (NodeNext). Under `tsx`,
  `./paths.js` loads `./paths.ts`; in a build the compiled file sits
  beside it.
- `strict`, `noUncheckedIndexedAccess` and `noImplicitOverride` are
  on. No `any`, no `@ts-ignore`.
- The library never prints. ESLint's `no-console` applies to
  `sync/**/*.ts` outside `cli/`, tests and harnesses; output goes
  through the event stream.

```bash
npm run build       # tsc: emit .js and .d.ts siblings
npm run typecheck   # this workspace, including tests
```

At the repo root, `npm run typecheck` covers `types`, `web`, `sync`,
`functions`, `desktop`, `tests/integration` and `tests`.

### Tests

| Command                             | What it runs                                                                                                                |
| ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `npm run test:unit`                 | Pure and in-process tests. No network.                                                                                      |
| `npm run test:integration`          | The integration suites against a real Firebase project (the shared sandbox by default), with a fresh user and org per test. |
| `npm run test:integration:emulator` | The same suites inside `firebase emulators:exec` (Firestore, Storage, Auth, Functions), after building `functions`.         |
| `npm test`                          | Unit, then integration.                                                                                                     |
| `npm run test:scale:*`              | On-demand scale suites against a real backend. See [docs/testing/scale-testing.md](../docs/testing/scale-testing.md).       |

The emulator script passes `--project doubling-compound-sandbox` on
purpose. Without it the emulators start on the `.firebaserc` default
(`doubling-compound-dev`) while the harness signs in against the
sandbox project. The Auth emulator then never sees the test user, the
minted token has no `email_verified` claim, and nearly every test
fails with `permission-denied`. That looks like a code break and is
not one. The emulator prints a project-mismatch warning, but it
scrolls past.

How the suites are organised, and how to wait on a round trip without
flaking, is in [DESIGN.md](DESIGN.md#testing).

### Publishing

`@doubling/compound-sync` is published by
[`publish-sync.yml`](../.github/workflows/publish-sync.yml) on a
GitHub Release whose tag starts with `@doubling/compound-sync@`, which
Changesets creates. It uses npm Trusted Publishing (OIDC, no stored
token), skips a version already on npm, and ships the same commit as
the desktop daemon. `prepack` runs the build, so the tarball holds
compiled JS. GitHub delivery is currently paused; see
[docs/developer/local-ci.md](../docs/developer/local-ci.md#github-delivery-is-paused).
Do not `npm publish` by hand.

The `exports` map re-exposes every `*.js` and `*.d.ts` subpath and
extensionless deep imports for both ESM and CommonJS. It blocks
`daemon-org-sync-wiring` (all three spellings), which ships in `files`
only so `agent.ts` can import it by relative path.
`sync/package-payload.test.ts` checks that the `files` allowlist
includes every module a shipped entry point imports.

## Related

- [DESIGN.md](DESIGN.md): how the daemon is built
- [cli/README.md](cli/README.md): the CLI contract
- [../DESIGN.md](../DESIGN.md): the whole system
- [docs/client/guides/sync-daemon.md](../docs/client/guides/sync-daemon.md):
  end-user guide
- [docs/security/09-sync-daemon.md](../docs/security/09-sync-daemon.md):
  security posture

---
_Source: https://npm.io/package/@doubling/compound-sync · Machine-readable twin of the npm.io package page. Health data is recomputed on every publish._
