@doubling/compound-sync
v1.16.6
Published
Bidirectional sync between Compound and local markdown files
Downloads
1,109
Readme
Compound Sync
Bidirectional sync between Compound and local markdown files. Edit files locally in your favorite editor and they sync to Compound in real time.
Quick Start
npx @doubling/compound-syncThat's it. A browser window will open for sign-in (Google or email/password), then the setup wizard will walk you through selecting your organization and sync folder.
Prerequisites
- Node.js (v18 or later)
TLS certificates
On Node 22+ the daemon auto-detects the OS system certificate store (/etc/ssl/cert.pem on macOS, /etc/ssl/certs/ca-certificates.crt or /etc/pki/tls/certs/ca-bundle.crt on Linux) and re-execs itself with NODE_EXTRA_CA_CERTS set; otherwise fetch() to Google Identity / Firebase Auth endpoints fails with auth/network-request-failed because Node's bundled CA store doesn't match what those services use.
If you're behind a corporate proxy with custom roots, set NODE_EXTRA_CA_CERTS explicitly to your bundle and the daemon will use that instead:
NODE_EXTRA_CA_CERTS=/path/to/corporate-bundle.pem npx @doubling/compound-syncWhat it does
Compound Sync watches a local folder and your Compound workspace simultaneously. Changes in either direction are synced automatically:
- Edit a file locally → it updates in Compound
- Edit a file in Compound → it updates locally
- Create or delete files in either place → synced
Local folder structure
{Sync Folder}/
{TeamName} Teamspace/ -- team files (bidirectional)
Private/ -- your private files (bidirectional)
Shared by Me/ -- symlinks to files you've shared
Shared with Me/ -- files shared with you (read-only)Hidden sync metadata
The daemon maintains per-machine state under your sync folder (dotfiles and directories, not uploaded to Compound):
| Path | Purpose |
| ---- | ------- |
| .compound-sync/manifest.json | Maps synced paths to Firestore file IDs (startup reconcile) |
| .compound-sync-state.json | Content-hash baseline for push/pull decisions |
| .compound-yjs-binding/ | Offline Yjs merge state per file |
Do not edit these by hand. See docs/features/sync/local-metadata.md.
Running
After setup, start syncing with:
npx @doubling/compound-syncThe sync daemon runs until you press Ctrl+C.
Multiple workspaces
If you're a member of more than one workspace, you can sync them all from a single daemon. During --setup you'll be asked which workspaces to sync (comma-separated or all) and given a per-workspace local folder prompt.
You can also keep multiple separate configs (one per workspace, or one for "all my workspaces") with --config pointing at an absolute path:
# Setup
npx @doubling/compound-sync --config ~/.config/compound-sync/work.json --setup
# Run
npx @doubling/compound-sync --config ~/.config/compound-sync/work.json--config accepts absolute paths (with ~ expanded), so you can keep configs outside the package install directory. Bare filenames still resolve relative to the package install for backward compatibility.
Security
- First sign-in is interactive (browser popup, Google or email/password). After that the daemon runs silently: it persists the Firebase refresh token locally so relaunches don't re-prompt.
- Persisted credentials live at
~/.config/compound-sync/{project}__{account}.json, written mode0600(owner read/write only) via an atomic tmp+rename. The desktop app overrides the path per-account withCOMPOUND_AUTH_FILE. Seeauth-persistence.ts. - The config file (
config.json,config-*.json) holds no credentials — onlyprojectIdand org→localPath mappings. - All data access uses the Firebase client SDK with App Check and the same Firestore/Storage rules as the web app — the daemon has no Admin-SDK elevation and cannot see beyond the signed-in user's org memberships. See ../docs/security/09-sync-daemon.md.
Internal Development
For Doubling team members testing against sandbox or dev environments:
# Setup
npx @doubling/compound-sync --env sandbox --config config-sandbox.json --setup
npx @doubling/compound-sync --env dev --config config-dev.json --setup
# Run
npx @doubling/compound-sync --env sandbox --config config-sandbox.json
npx @doubling/compound-sync --env dev --config config-dev.jsonConfig files (config-*.json) are gitignored and stored locally.
Tests
# Pure unit tests (no emulator required)
npm run test:unit
# Integration tests (boots firestore + storage emulators)
npm run test:integration
# Both
npm testTypeScript conventions (DOU-181)
sync/ is being migrated to TypeScript file-by-file. The conventions:
- Source layout: in-place. Each
.tsfile emits.js,.d.ts, and source maps as siblings viatscwithoutDir: ".". The emitted.jsis a gitignored build artifact, never edited by hand; the.tsis the source. - Imports always use the
.jsextension (NodeNext convention). At runtime: in dev/test thetsxloader maps./paths.jsto./paths.tsso source runs without a build step; in production the compiledpaths.jsexists alongside and resolves directly. - Dev (no build needed):
npm run syncruns throughnode --import tsx, so.tsfiles load on demand. - CI / publish:
npm run buildcompiles every.tssource to its.jssibling.prepackruns build automatically beforenpm publishso the npm tarball ships compiled JS. The desktop staging script also runsnpm run -w sync buildbefore copying. - Typecheck:
npm run typecheck(alias fortsc --noEmit) at the workspace level, or at repo rootnpm run typecheckwhich runs web + sync together. - Strictness:
strict: true,noUncheckedIndexedAccess: true,noImplicitOverride: true. Noany. No// @ts-ignore. If a third-party module lacks types, declare its shape in a.d.tsor PR types upstream.
Logging convention
Org-scoped output: [${orgId}] [tag] message. Use console.warn for recoverable issues, console.error for failures. Tags like [local scan] and [manifest reconcile] identify subsystems.
Phase 1 migrated paths.js to paths.ts as proof of pattern. Phase 3 ([[DOU-183]]) migrates the remaining source files; everything in sync/ will be .ts by the end.
Building & running from source
sync.js (and the sibling *.js files listed in package.json's files) are build artifacts, gitignored, never edited by hand. Run from the .ts source with the tsx loader:
# Dev (no build; tsx maps ./foo.js → ./foo.ts on demand)
npm run sync -- --setup --env sandbox # first-run wizard against sandbox
npm run sync -- --env sandbox # start the daemon
# Compile every .ts to its .js/.d.ts sibling (tsc, outDir ".")
npm run build
# Typecheck only
npm run typecheck--env takes a short name (sandbox | dev | prod); it is expanded to doubling-compound-<name> internally (sync.ts). Omitting --env defaults to prod.
Publishing to npm
@doubling/compound-sync is published automatically by .github/workflows/publish-sync.yml on every GitHub Release whose tag starts with @doubling/compound-sync@ (Changesets creates the release + version bump). Highlights:
- Trusted Publishing (OIDC): no long-lived
NPM_TOKEN— GitHub mints a short-lived OIDC token that the npm CLI (≥ 11.5.1) exchanges for a publish-scoped token. prepackrunsnpm run build, so the tarball ships compiled JS.- Idempotent: the workflow checks
npm viewand skips cleanly ifsync/package.json's version is already on npm. - Kept in lockstep with the desktop
.dmgbuilt from the same commit (DOU-159), sonpx @doubling/compound-syncand the bundled daemon are identical.
Do not npm publish by hand; cut a Changesets release instead.
Design docs
- Sync daemon design: DESIGN.md
- System-wide context: ../DESIGN.md (§7 sync/conflict model)
- Feature docs: ../docs/features/sync/index.md, local-metadata.md, sync-manifest-design.md
- Security: ../docs/security/09-sync-daemon.md
