DropBear

A self-hosted file-sync daemon that pairs local folders with any S3-compatible bucket — with a daemon, web dashboard, menu-bar app, and native Android client.

Kind

Storage Utility

Stack

Go, SQLite, S3

Status

beta

LAPTOP MOBILE USB DRIVE SERVER BUCKET S3 · R2 · B2 ›› CONTENT-ADDRESSED files keyed by hash — automatic deduplication TOMBSTONED DELETES removals propagate safely, never silently resurrect SAFE CONFLICTS two edits? both versions survive — no overwrite REMOVABLE DEVICES 1st-class USB & external drives, mounted or not TRANSPARENT LAYOUT files map 1:1, readable without DropBear

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.

Merging Honestly

The interesting engineering is in the failure modes, not the happy path.

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.

DropBear menu-bar app showing the daemon running, an online root, and an Open Dashboard action
Menu-bar app — daemon state, per-root status, one-click dashboard
DropBear web dashboard showing recent sync ticks, byte counts, and recent file changes for a root
Embedded dashboard — live ticks, transfer counts, recent changes

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.

Content-Addressed SHA-256 blobs + JSON manifests on plain object storage
BYOB R2, B2, S3, MinIO — storage decoupled from sync
Multiple Interfaces CLI · Daemon · Web · Tray · Android
Materialized Views Download files as needed