The Problem
Hosted sync services — Dropbox, Drive, iCloud — work well until they don't: storage tiers you didn't ask for, files quietly held hostage behind a renewal, telemetry baked into the client. The mechanics underneath are not complicated: a folder, a bucket, and a careful enough merge. The hard part is doing that merge honestly.
The Approach
DropBear synchronizes a local folder with any S3-compatible bucket. Storage is decoupled from sync, so the same binary works against Cloudflare R2, Backblaze B2, AWS S3, or a self-hosted MinIO — whichever is cheapest, closest, or most trusted that week. The bucket is never treated as a filesystem: it holds content-addressed (SHA-256) blobs, one JSON manifest per device, and per-device heads. Local state lives in SQLite; each folder carries its own identity in a .dropbear/ directory.
- Bidirectional sync with explicit tombstones for deletes — absence is never read as "delete this everywhere."
- Conflict files, not silent overwrites. When two devices touch the same file, both versions survive on disk and neither is published until you resolve it.
- Sync modes picked per root: two-way, upload-only, download-only, or archive-only.
- Single static binary. Drop it on a host, point it at a bucket, done.
Merging Honestly
The interesting engineering is in the failure modes, not the happy path.
- Removable-drive safety. A missing root reads as offline, never as a mass deletion — pull the USB drive and DropBear waits instead of propagating a delete storm.
- Crash-safe deletes. Tombstone garbage collection runs a journaled
PUT pending → PUT real → DELETE pending → commitlifecycle, and reconciles on startup from a crash between any two steps without burning a sequence number. - Drift detection.
statuscross-checks every manifest entry — local and peer — against the blobs actually present in the bucket, so a half-uploaded sync surfaces instead of silently corrupting a restore.
Daemon, not Cron
dropbear daemon supervises any number of roots in a single process — one goroutine per root, each with a recursive fsnotify watcher (debounced) and an adaptive remote-head poller that backs off when idle and snaps back the moment a peer publishes. Signals get two-strike handling (first asks for a graceful drain, second forces it); a panic in one root is recovered and isolated so it can't take down the others. Everything is structured log/slog output with a versioned JSON envelope.
Observability
The daemon exposes a loopback-only status API on 127.0.0.1, gated by a crypto/rand bearer token handed off through a 0600 runtime file. Two clients consume it: a one-page web dashboard embedded in the binary, and a native macOS menu-bar app. The core dropbear binary stays pure-Go and CGo-free — all the CGo needed for the tray is confined to its own command.


Materialized Views
Not every device wants every byte. A file can be a zero-byte stub (marked with an xattr) that stands in for remote content until you ask for it. shelve turns local files back into stubs after verifying the blob is reachable; fetch materializes them on demand. It's the "online-only file" pattern from the hosted services — but built on plain xattrs and the existing content-addressed store, with no kernel extension or proprietary filesystem.
Going Mobile
A native Android client ships alongside the desktop stack, and it deliberately does not try to be the daemon on a smaller screen. Phones have metered connections, aggressive process death, and no business holding a full copy of a bucket. So the app talks straight to S3 with its own device-scoped credentials, lists the namespace from the (tiny) manifests, and pulls a blob only when you actually open a file — the shelved-by-default case, with online-only files as the normal state rather than the exception.
Being read-only is the design, not a gap: the app never writes manifests, so it can't publish a conflict, can't strand a tombstone, and is safe to browse while a desktop root is mid-sync. The costs are honest ones — no uploads, no client-side encryption (bucket ACLs are the security boundary), no per-root credential scoping without a broker in front, and no iOS build.
Current Status
A daily driver for my own cross-device sync, well past the prototype stage but pre-1.0. The CLI, multi-root daemon, status API, web dashboard, menu-bar app, and Android client all work today; the daemon self-installs as a systemd or launchd service. What's intentionally not done yet: the tray's start/stop toggle is still a stub, and there's no packaged installer or signed app bundle — installation is build-it-yourself, with the Android app side-loaded by APK (or tracked through Obtainium) rather than listed in a store. The current focus is promoting the read-only status API into a small local control plane so the dashboard and tray can drive sync, fetch, and shelve directly. Single-user, self-hosted, and no telemetry remain non-negotiable by design.