Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

127 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

devbox

๐Ÿ“ฆ Continuous file sync for developers โ€” like Dropbox, but it respects your .devignore, refuses to leak secrets, runs your hooks, and keeps Git-like history. ๐Ÿ“ฆ


status release dashboard install language license platforms

conflicts secrets hub dedup

        โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—
        โ•‘   โ”Œโ”€โ” โ”Œโ”€โ” โ”ฌ  โ”ฌ โ”Œโ” โ”Œโ”€โ” โ”ฌ โ”ฌ                                 โ•‘
        โ•‘   โ”‚โ”‚โ”€โ”ค โ”œโ”ค  โ”‚  โ”‚ โ”œโ”ดโ”โ”‚ โ”‚ โ”‚โ”ดโ”ค   sync your dev tree,           โ•‘
        โ•‘   โ”ดโ”€โ”˜โ”€โ”˜โ””โ”€โ”˜  โ””โ”€โ”€โ”˜ โ””โ”€โ”˜โ””โ”€โ”˜ โ”ด โ”ด   skip the junk, keep history  โ•‘
        โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

Note

๐ŸŽ‰ devbox v1 is feature-complete (M0โ€“M7.6, fleet-verified on real hardware โ€” Macs + arm64 Pis live-syncing through a hub on a NAS) and v2 has begun. ๐Ÿ”ฎ The v1 spec is fully implemented; the v2 spec sequences M8โ†’M11 โ€” M8 foundations are landing now. Star/watch and follow along. โญ

Milestone Status
M0 โ€” Skeleton (CLI, identity, config) โœ… done
M1 โ€” Watch ยท .devignore ยท secret-guard ยท chunking ยท manifest โœ… done
M2 โ€” Hub + one-way push (deployed + verified cross-machine ๐Ÿ›ฐ๏ธ) โœ… done
M3 โ€” Two-way sync ยท SSE fan-out ยท 3-way conflict copies ยท live daemon โœ… done โ€” two real Macs sync live through the hub ๐Ÿ”„
M4 โ€” Read-only mounts ยท sub-path mounts ยท bandwidth cap โœ… done โ€” fleet-verified
M5 โ€” Lifecycle hooks (pre/post push/pull, on-conflict) โœ… done โ€” post-pull ran on a real fleet node ๐Ÿช
M6 โ€” Versioning: log / restore + hub GC โœ… done โ€” restore reverted a file on the fleet ๐Ÿ•ฐ๏ธ
M6.5 โ€” devbox deploy (pin a mount to a snapshot, no push) โœ… done โ€” blue/green-deployed v1 on a real box while head stayed v2 ๐Ÿš€
M7 โ€” Hardening: devbox doctor, reconnect/backoff, rescan fallback, name-clash, release builds โœ… done โ€” doctor/stop/hooks + share-name guard + dead-watcher rescan fleet-verified ๐Ÿ›ก๏ธ
M7.5 โ€” Adversarial security/data-loss audit + fixes (path-traversal, blob integrity, never-clobber, safe GC) โœ… done โ€” 26 findings, all promise-breakers fixed, race-clean ๐Ÿ”
M7.6 โ€” Hardening: fsync durability ยท DoS caps + timeouts ยท pidfile PID-reuse guard ยท join proof-of-possession โœ… done โ€” fleet-verified on arm64 ๐Ÿ›ก๏ธ
๐Ÿ”ฎ M8 โ€” v2 Foundations: migration runner ยท per-(share,id) snapshots ยท control socket + pause/resume ยท M8a teams ยท restore byte-safety โœ… justified-now scope complete & fleet-verified (Pi .13 owner invited an editor, Pi .15 redeemed & pushed); 3 migrations verified on a copy of the real hub DB. M9โ€“M11 stay demand-driven ๐Ÿ—๏ธ๐Ÿ‘ฅ

๐Ÿ“‘ Table of Contents

๐Ÿค” Why devbox? ๐Ÿง  Core Concepts ๐Ÿ—๏ธ Architecture
๐Ÿ”„ How Sync Works ๐Ÿ’ฅ Conflicts โšก Install & Run
๐Ÿงฐ CLI Reference ๐Ÿ“Š Live Dashboard ๐Ÿช Hooks
๐Ÿ•ฐ๏ธ Versioning & Deploy ๐Ÿ–ฅ๏ธ Cross-Platform ๐Ÿ” Security & Durability
๐Ÿ—บ๏ธ Roadmap โš–๏ธ License & Open-Core ๐Ÿ™Œ Contributing

๐Ÿค” Why devbox?

You've got a MacBook, a 40-node Pi cluster, and a TrueNAS box. You want your active working tree mirrored across them โ€” right now, automatically โ€” without committing half-done work.

Your options today all hurt:

ToolThe pain ๐Ÿ˜–
โ˜๏ธ Dropbox / iCloud Syncs everything blindly โ€” chokes on node_modules, happily uploads your .env, thrashes on build artifacts.
๐Ÿ™ Git Manual. Commit-based. Not built to live-mirror an in-progress working tree. Branching โ‰  syncing.
๐Ÿ“ rsync / scp One-shot, one-direction, no history, no hooks, no conflict safety. You babysit it.
๐Ÿ” Syncthing Great P2P sync โ€” but no .devignore dev-ergonomics, no lifecycle hooks, no snapshot/deploy story.

โœจ devbox is the missing layer

๐ŸŽฏ Feature
๐Ÿ”„ Continuous bidirectional sync across all your machines
๐Ÿ™ˆ .devignore (gitignore syntax) โ€” skip node_modules, dist, the junk
๐Ÿ” Default-on secret guard โ€” hard-refuses to upload .env, keys, secrets
๐Ÿช Lifecycle hooks โ€” pnpm install / restart a container when files land
๐Ÿ•ฐ๏ธ Git-like snapshots per share โ€” roll back any file, any time
๐ŸŽš๏ธ Selective mounts โ€” cherry-pick a share, or just one sub-path, per machine
๐Ÿ’ฅ Conflict-safe โ€” never blocks, never asks, never loses a byte
๐Ÿ  Self-hostable โ€” one Go binary on your Pi/TrueNAS, no SaaS required
๐ŸŒ Cross-platform โ€” Linux ยท macOS ยท Windows

๐Ÿง  Core Concepts

mindmap
  root((๐Ÿ“ฆ devbox))
    ๐Ÿ›ฐ๏ธ Hub
      publishes shares
      stores chunks + manifests CAS
      brokers change events
      one owner ยท many devices
    ๐Ÿ’ป Device
      Ed25519 identity
      joins with a token
      runs one daemon
    ๐Ÿ“‚ Share
      named tree projects/ repos/ services/
      unit of snapshot history
    ๐Ÿ”— Mount
      share + subpath to local path
      read-write or read-only
    ๐Ÿ“ธ Snapshot
      immutable BLAKE3 manifest
      per share history
Loading
Term What it is
๐Ÿ›ฐ๏ธ Hub The server. Publishes shares, stores content-addressed chunks + manifests, brokers events. One owner, many devices. Self-hosted single binary.
๐Ÿ’ป Device A machine with an Ed25519 identity, joined to a hub. Runs one daemon (devboxd).
๐Ÿ“‚ Share A named top-level tree on the hub (projects, repos, services). The unit of permission & history.
๐Ÿ”— Mount A device-side binding: share[/subpath] โ†’ localpath, read-write or read-only. One daemon watches many.
๐Ÿ“ธ Snapshot An immutable, per-share manifest version (id = BLAKE3 of the manifest).

๐Ÿ—๏ธ Architecture

flowchart LR
    subgraph LAP["๐Ÿ’ป MacBook"]
        D1["devboxd ๐Ÿ›ก๏ธ"]
        M1["๐Ÿ“‚ projects/"]
        M2["๐Ÿ“‚ repos/"]
    end

    subgraph HUB["๐Ÿ›ฐ๏ธ Hub  ยท  Pi / TrueNAS"]
        WS{{"๐Ÿ”Œ WebSocket\nchange events"}}
        HTTP{{"๐ŸŒ HTTP\nblob GET/PUT"}}
        CAS[("๐ŸงŠ CAS\nchunks + manifests\nBLAKE3")]
        DB[("๐Ÿ—„๏ธ SQLite WAL\nshares ยท snapshots\ndevices ยท tokens")]
        MET["๐Ÿ“Š /metrics + status"]
    end

    subgraph PI["๐Ÿ“ pi-07"]
        D2["devboxd ๐Ÿ›ก๏ธ"]
        M3["๐Ÿ“‚ projects/p22/backend\n(read-only โ†’ /var/www)"]
    end

    M1 & M2 -->|"events"| WS
    M1 & M2 -->|"blobs"| HTTP
    WS --> CAS
    HTTP --> CAS
    CAS --- DB
    WS -->|"events"| D2
    HTTP -->|"blobs"| D2
    D2 --> M3

    classDef hub fill:#0d1117,stroke:#4F9CF9,stroke-width:2px,color:#fff;
    class HUB,WS,HTTP,CAS,DB,MET hub;
Loading

๐Ÿ’ก Why two channels? WebSocket for live events (tiny, ordered) + stateless HTTP for blobs (GET /blob/<hash> โ†’ range/resume/caching for free). Same TLS endpoint, same token. Pure-WebSocket would force us to reinvent resumable transfer over a socket โ€” more code.


๐Ÿ”„ How Sync Works

sequenceDiagram
    autonumber
    participant FS as ๐Ÿ“ Filesystem
    participant D as ๐Ÿ›ก๏ธ devboxd
    participant H as ๐Ÿ›ฐ๏ธ Hub
    participant P as ๐Ÿ“ Peer

    FS->>D: โœ๏ธ file changed (fsnotify, debounced ~300ms)
    D->>D: ๐Ÿ™ˆ .devignore + ๐Ÿ” secret-guard filter
    D->>D: ๐Ÿงฉ FastCDC chunk โ†’ BLAKE3 โ†’ manifest diff
    D->>D: ๐Ÿช pre-push hook (can veto ๐Ÿ›‘)
    D->>H: โฌ†๏ธ upload missing chunks (HTTP, bw-capped)
    H->>H: ๐Ÿ“ธ append snapshot, advance share HEAD
    H-->>D: โœ… ack
    D->>D: ๐Ÿช post-push hook
    H->>P: ๐Ÿ”” change event (WebSocket)
    P->>P: ๐Ÿช pre-pull hook (can veto ๐Ÿ›‘)
    P->>H: โฌ‡๏ธ fetch missing chunks (HTTP)
    P->>P: ๐Ÿงฑ reassemble โ†’ โš›๏ธ atomic rename into place
    P->>P: ๐Ÿช post-pull hook (pnpm install / restart ๐Ÿš€)
Loading

A read-only mount skips steps 1โ€“8 (it never pushes) but still receives and applies inbound changes. ๐Ÿ”’


๐Ÿ’ฅ Conflicts: Never Lose a Byte

devbox is Dropbox-easy (never nags you mid-work) and data-loss-proof. Here's the magic: the hub keeps a linear HEAD per share, and every push declares the snapshot it was based on.

flowchart TD
    START([๐Ÿ“ค push arrives]) --> Q{parent == HEAD?}
    Q -->|โœ… yes| FF["โฉ fast-forward\nadvance HEAD"] --> DONE([๐ŸŽ‰ done])
    Q -->|โŒ no| THREE["๐Ÿ”ฑ per-file 3-way\nvs common ancestor"]
    THREE --> ONLY{changed by\nonly the pusher?}
    ONLY -->|yes| CLEAN["โœจ applies cleanly"] --> DONE
    ONLY -->|both sides| CONF["๐Ÿ’ฅ CONFLICT"]
    CONF --> KEEP["๐Ÿ‘‘ first-to-land stays canonical"]
    CONF --> COPY["๐Ÿ“ loser saved as\nfoo.conflict-laptop-1719.go"]
    CONF --> HOOK["๐Ÿ”” on-conflict hook fires"]
    KEEP & COPY & HOOK --> SAFE([๐Ÿ›Ÿ zero data lost])

    style CONF fill:#5a1e1e,stroke:#ff6b6b,color:#fff
    style SAFE fill:#1e5a2e,stroke:#51cf66,color:#fff
    style DONE fill:#1e5a2e,stroke:#51cf66,color:#fff
Loading
๐Ÿ“– The classic "laptop was offline" scenario โ€” click to expand

Both machines synced at snapshot S3, both have foo.go v1:

  1. ๐Ÿ”Œ Laptop goes offline, edits foo.go โ†’ v2-laptop (queued, parent still S3).
  2. ๐Ÿ“ Pi (online) edits the same foo.go โ†’ v2-pi, pushes. Hub HEAD S3 โ†’ S4. Pi's wins canonical.
  3. ๐Ÿ”Œ Laptop reconnects, pushes with parent=S3 โ€” but HEAD is S4. parent โ‰  HEAD โ†’ conflict path.
  4. ๐Ÿ”ฑ Both changed foo.go since S3 โ†’ real conflict.
  5. ๐Ÿ›Ÿ Result, nothing destroyed: both machines end up with foo.go = v2-pi (canonical) and foo.conflict-laptop-1719.go = v2-laptop beside it.

The offline edit is never lost โ€” it just lands as a clearly-named sibling. ๐ŸŽฏ

Conflict rules at a glance:

Situation Outcome
๐Ÿค Both edit same file First-to-land canonical; loser โ†’ .conflict-<host>-<ts> copy
๐Ÿ—‘๏ธ One deletes, one edits Edit always wins โ€” a delete has no bytes to lose
๐Ÿ”ค Rename Free โ€” old-gone + new-added; content-addressed = zero re-transfer
๐Ÿ”’ Read-only mount about to clobber a local edit Local stashed as .conflict-local-<ts> first
๐Ÿ‘€ You find out via devbox status badge ยท on-conflict hook ยท devbox conflicts list

๐Ÿšซ No blocking prompts. A headless daemon can't prompt you, and nagging would break the whole "Dropbox-easy" promise. You get told; you choose when to look.


โšก Install & Run

Client (macOS / Linux) โ€” one line. Detects your platform, lets you pick where the binary lives, and offers a keep-alive auto-restart service (launchd KeepAlive on mac, systemd Restart=always on Linux):

curl -fsSL https://git.shoemoney.ai/shoemoney/devbox-dist/releases/download/latest/install.sh | sh
# non-interactive: DEVBOX_BIN_DIR=~/.local/bin DEVBOX_SERVICE=1 sh install.sh
# also install the hub: sh install.sh --hub
๐ŸชŸ Windows ยท ๐ŸŽ macOS Full Disk Access ยท ๐Ÿ› ๏ธ knobs
  • Windows: irm https://git.shoemoney.ai/shoemoney/devbox-dist/releases/download/latest/install.ps1 | iex โ€” copies devbox.exe, adds it to PATH, and (optionally) a restart-on-failure Scheduled Task at logon. Notes Controlled Folder Access if you have it on.
  • From Go (have a toolchain): go install github.com/shoemoney/devbox/cmd/devbox@latest (hub: โ€ฆ/cmd/devbox-hub@latest).
  • macOS Full Disk Access: to sync ~/Desktop, ~/Documents, ~/Downloads, or iCloud, grant Full Disk Access to the devbox binary (System Settings โ†’ Privacy & Security โ†’ Full Disk Access). devbox doctor tests your actual mounts and tells you the exact fix + a deep link โ€” a background daemon can't show the per-folder prompt, so without it those mounts silently fail.
  • Knobs: --bin-dir DIR ยท --hub ยท --service / --no-service ยท --release-url URL (or the DEVBOX_* env equivalents). No prebuilt release? The script falls back to a local dist/ (run scripts/build-release.sh) or go build.
  • Service control: systemctl --user {status,disable} devbox (Linux) ยท launchctl unload ~/Library/LaunchAgents/ai.shoemoney.devbox.plist (mac).

Hub on a NAS (TrueNAS / Synology / unRAID / any Docker host) โ€” self-healing container, drop-in:

docker compose up -d --build                          # hub on :8088, data persisted, restart: unless-stopped
docker compose exec hub devbox-hub token --data /data # mint a join token
# live dashboard: uncomment the :8099 port + --dashboard command in docker-compose.yml
# stronger self-heal (restart on unhealthy): docker compose --profile selfheal up -d

The image is a tiny (~32 MB) static, non-root Alpine build โ€” Dockerfile + docker-compose.yml at the repo root.

๐Ÿ”’ Production: TLS + safe upgrades

Put the hub behind TLS. The hub speaks plain HTTP and the bearer token + your file bytes cross the wire in the clear โ€” front it with a reverse proxy (nginx / Caddy / Nginx Proxy Manager) and terminate TLS there (the API is bearer-authed). Two settings a sync hub needs in the proxy:

location / {
    proxy_pass http://<hub-host>:8088;
    client_max_body_size 0;     # large blob uploads โ€” never cap them
    proxy_buffering off;        # stream the /v1/events SSE to clients
    proxy_read_timeout 3600s;   # long-lived event stream
}

Devices then devbox join https://hub.example.com <token> โ€” Go's client does TLS natively.

Upgrade safely. The hub DB is the canonical index to every share's history, so don't hot-swap the binary. deploy/redeploy-hub.sh runs the never-YOLO pipeline: migration dry-run on a copy of live data (scripts/verify-hub-migration.sh) โ†’ auth smoke the new binary in a throwaway instance (scripts/hub-auth-smoke.sh) โ†’ back up binary + DB โ†’ swap โ†’ verify the new code is live โ†’ auto-rollback on any failure. (--gc-every <dur> self-maintains so blobs don't accumulate.)


๐Ÿš€ Quick Start

โœ… Every command below is implemented and fleet-tested. Build with scripts/build-release.sh.

# ๐Ÿ›ฐ๏ธ  On your hub (Pi / TrueNAS / NAS)
devbox-hub serve --data /srv/devbox --listen 0.0.0.0:8088
devbox-hub token                                # prints a one-time join token

# ๐Ÿ’ป  On your laptop
devbox join http://hub.lan:8088 <token>         # enroll this machine
devbox publish ~/Projects projects              # create a share from a folder
devbox start                                    # live-sync daemon (foreground)

# ๐Ÿ“  On another machine โ€” clone the share and keep it in sync
devbox join http://hub.lan:8088 <token>
devbox mount projects ~/Projects                # clone + register the mount
devbox start

# ๐Ÿš€  A read-only deploy box โ€” pulls, never pushes its runtime cruft back up
devbox mount api /var/www/api --ro
devbox start

That's it. โœจ Edit on your laptop โ†’ it lands on the Pi in near-real-time, node_modules stays home, your .env never leaves the building, and post-pull can pnpm install + restart your container automatically. ๐Ÿช„


๐Ÿงฐ CLI Reference

๐Ÿ’ป Device commands
Command What it does
devbox setup ๐Ÿงญ Step-by-step first-run wizard (also auto-offered once on a fresh machine; opt out with "don't ask again")
devbox join <hub> <token> ๐ŸŽŸ๏ธ Enroll this device against a hub
devbox mount <share> <dir> ๐Ÿ”— Mount a share into a local dir (clone + sync)
devbox mount <share> <dir> --ro ๐Ÿ”’ Mount read-only (pull only, never push)
devbox mount โ€ฆ --exclude <pat> ๐Ÿ™ˆ Device-local ignore (gitignore syntax, repeatable) layered on the shared .devignore โ€” e.g. skip a local build dir
devbox publish <dir> <name> ๐Ÿ“‚ Create a share from a local folder + push it
devbox unmount <share> โ๏ธ Stop syncing a mount (files stay on disk)
devbox start / stop โ–ถ๏ธโน๏ธ Run / stop the daemon
devbox status [--json] ๐Ÿ“Š Device, hub, mounts (with ro/pinned), per-mount sync-age and last sync error (โš ๏ธ last sync failed: โ€ฆ); โš ๏ธ says when the daemon isn't running (no silent stale state); --json exposes last_err (prefers live daemon state)
devbox log <share> [--json] ๐Ÿ•ฐ๏ธ Snapshot history (full ids); --json for machine-readable output
devbox restore <share> <snap> [path] โ†ฉ๏ธ Roll back a file or a whole share
devbox deploy <share> <snap> ๐Ÿš€ Pin a mount to a snapshot โ€” applies it without pushing (blue/green)
devbox conflicts [--json] [--rm] ๐Ÿ’ฅ List conflict copies across all mounts; --json emits a JSON array, --rm deletes them (review first!)
devbox ignore <pattern> ๐Ÿ™ˆ Append a pattern to ./.devignore (rejects a pattern that doesn't compile)
devbox hook edit <share> <event> ๐Ÿช Scaffold/open a hook in $EDITOR; hook list <share> shows installed
devbox doctor [--json] ๐Ÿฉบ Diagnose watcher limits, perms, bash, hub connectivity + bearer + clock skew vs hub (warns >30s) + free disk on each mount/config dir (warns <1 GiB); non-zero exit on โŒ โ€” cron-friendly; --json for monitoring
devbox status shows sync age โฑ๏ธ Live status now prints per-mount last-sync age ("synced 12s ago" / "not synced yet")
devbox pause [--for <dur>] / resume โธ๏ธโ–ถ๏ธ Suspend/resume the running daemon's syncing via its control socket; --for 2h auto-resumes (M8)
devbox invite <share> <principal> <role> โœ‰๏ธ Mint an invite token granting a role (--reshare for +s); attenuation-enforced (M8a)
devbox invite revoke <token> ๐Ÿ—‘๏ธ Kill a pending invite before it's redeemed (only a caller who could mint it)
devbox members <share> ๐Ÿ‘ฅ Show who can access a share, or "legacy share" (M8a)
devbox-hub member set/rm/list ยท principal ๐Ÿ›ก๏ธ Hub-side role admin (M8a)
devbox peers ๐ŸŒ Planned โ€” needs a hub peers endpoint (M10)
๐Ÿ›ฐ๏ธ Hub commands (run on the hub host)
Command What it does
devbox-hub serve --config <file> ๐Ÿš€ Start the hub
devbox-hub token ๐ŸŽŸ๏ธ Mint / rotate the join token
devbox-hub device ls [--json] ยท revoke <id> ๐Ÿ“‹โŒ List enrolled devices (id/name/principal/last-seen/revoked) / revoke one
devbox-hub share ls [--json] ๐Ÿ“‘ List shares (name/head/ACL mode/#snapshots/#members/updated)
devbox-hub readonly <device> <share> ๐Ÿ”’ Mark a device read-only on a share
devbox-hub member set/rm/list ยท principal ๐Ÿ›ก๏ธ Per-share roles + principals (M8a)
devbox-hub backup <dir> ๐Ÿ’พ Disaster-recovery snapshot: consistent DB copy (VACUUM INTO) + the blob tree into <dir> โ€” flocked against gc so a sweep can't strand it mid-copy
devbox-hub fsck [--json] ๐Ÿ”ฌ Integrity scan: re-hash every blob (bit-rot) and flag dangling snapshots (manifest โ†’ missing blob); non-zero exit on either โ€” pairs with backup for DR confidence
devbox-hub serve --dashboard ๐Ÿ“Š Serve the live web dashboard (loopback :8099 by default)
devbox-hub serve --dashboard-token <tok> ๐Ÿ” Require a token to view the dashboard (recommended for any non-loopback bind)
devbox-hub serve --metrics-token <tok> ๐Ÿ” Require a token for /metrics โ€” close the unauthenticated leak on a WAN-exposed hub
devbox-hub serve --access-log ๐Ÿ“ Log one line per request (method, path, status, bytes, addr, device id) for WAN forensics
devbox-hub serve (startup) ๐Ÿ›ฐ๏ธ Logs build version + dashboard state; โš ๏ธ warns if --listen is non-loopback (plain HTTP โ†’ cleartext bearers without the TLS proxy)
devbox-hub serve (SIGTERM) ๐Ÿ›‘ Graceful shutdown โ€” drains in-flight requests via http.Server.Shutdown and drops live SSE streams immediately (no 10s wait), so the hardened redeploy swaps cleanly
devbox-hub serve --gc-every <dur> ๐Ÿงน Opt-in in-process periodic GC (off by default; each sweep animates on the dashboard)
devbox-hub gc [--dry-run] [--keep <n>] [--keep-days <n>] ๐Ÿงน GC unreferenced chunks; --dry-run previews, --keep keeps N newest/share, --keep-days also keeps anything from the last N days
GET /healthz ยท GET /readyz ๐Ÿฉบ Liveness (/healthz reports the build version) / readiness (/readyz pings the DB โ†’ 503 if unreachable) for Docker/LB
GET /metrics ๐Ÿ“Š Prometheus: gauges (devices/shares/snapshots/chunks) + counters (blob bytes in/out, pushes, conflicts); gate with --metrics-token

๐Ÿ“Š Live Dashboard โ€” watch your fabric breathe

Our own. No Grafana, no Prometheus required, no external deps โ€” it ships inside the hub binary and runs air-gapped. Off by default; flip it on and watch every push, join, and chunk flow in real time. โœจ

devbox hub live dashboard

devbox-hub serve --dashboard                       # loopback http://127.0.0.1:8099 (safe default)
devbox-hub serve --dashboard --dashboard-addr 0.0.0.0:8099   # expose on the LAN (unauthenticated โ€” warns you)
devbox-hub serve --dashboard --gc-every 24h        # also self-maintain (in-process GC) โ€” each sweep animates

A mission-control flow visualization rendered on <canvas> at 60fps: a breathing hub core, your devices orbiting (bright = active), shares on the outer ring โ€” and the fabric comes alive with five flow events: push (device โ†’ hub โ†’ share, fans out to subscribers), pull (a calm teal pulse inbound as a device syncs), conflict (a red collision burst when a stale push is rejected), gc (an amber hub-wide sweep

  • toast when garbage-collection runs), and join. Plus live metric tiles and a server-side history sparkline (stacked push/pull/conflict/gc per minute, last hour) that survives a page reload, and a streaming, color-coded activity feed.
How it works
๐Ÿ–ฅ๏ธ Served from the hub single go:embed'd HTML, vanilla JS + canvas, zero CDN/build
๐Ÿ”Œ GET /api/state snapshot: hub, totals, devices, shares + a 60-minute history window (UI polls every 10s)
๐Ÿ“ก GET /api/events SSE live flow stream (join ยท push ยท pull ยท conflict ยท gc) โ€” the dashboard animates each one
๐Ÿ”’ Loopback by default unauthenticated read-only metrics; bind non-loopback only deliberately (it warns)
๐ŸŽฌ ?demo=1 synthesizes a live stream of all five event types โ€” instant wow + works offline (file://)
flowchart LR
    DEV["๐Ÿ’ป device pushes"] -->|"POST /v1/push"| HUB["๐Ÿ›ฐ๏ธ hub"]
    HUB -->|"Emit(join/push)"| BRK["๐Ÿ“ก SSE broker"]
    BRK -->|"/api/events"| UI["๐Ÿ“Š dashboard canvas"]
    HUB -->|"/api/state"| UI
    UI -.->|"particle deviceโ†’hubโ†’share"| UI
    style HUB fill:#0b2b3a,stroke:#27e8ff,color:#fff
    style UI fill:#2a0b2b,stroke:#ff3df0,color:#fff
    style BRK fill:#1e5a2e,stroke:#b6ff3a,color:#fff
Loading

๐Ÿช Hooks

Drop executable scripts in <mount>/.devbox/hooks/, named after the event. bash everywhere (a .ps1 hook auto-runs via pwsh on Windows ๐ŸชŸ). pre-* non-zero exit aborts the step. 60s timeout โ€” a hung hook is killed, never wedges the loop. โฑ๏ธ

Hook Fires Abort? Typical use
pre-push before upload โœ… ๐Ÿงน lint/format, secret scan
post-push after upload โŒ ๐Ÿ“ฃ notify, log, tag a snapshot
pre-pull before apply โœ… ๐Ÿ›‘ stop a container / dev server
post-pull after apply โŒ ๐Ÿ“ฆ pnpm install, migrate, restart
on-conflict conflict copy made โŒ ๐Ÿ”” open a diff, ping you, log
#!/usr/bin/env bash
# .devbox/hooks/post-pull  โ€”  reinstall deps + restart only when needed
if grep -qE 'package\.json|pnpm-lock\.yaml' "$DEVBOX_CHANGED_FILES"; then
  pnpm install --frozen-lockfile
fi
docker compose restart app   # ๐Ÿš€
๐ŸŒฑ Injected environment variables
DEVBOX_EVENT=post-pull
DEVBOX_MOUNT=/srv/project
DEVBOX_SHARE=projects
DEVBOX_HOST=pi-node-07
DEVBOX_CHANGED_FILES=/tmp/devbox-changes.txt   # newline-delimited
DEVBOX_SNAPSHOT=ab12cd34
DEVBOX_REMOTE=hub.shoemoney.ai

๐Ÿ™ˆ .devignore & Secret Guard

๐Ÿ™ˆ .devignore โ€” gitignore syntax, shared at the share root

node_modules/      # ๐Ÿ“ฆ the usual suspects
dist/  build/  .next/  target/
*.log  *.tmp  .DS_Store
!.env.example      # โ— negate to force-include

Matched paths are invisible to sync in both directions. Change it โ†’ rescan; newly-ignored files are left on disk (never deleted), they just stop syncing.

๐Ÿ” Secret Guard โ€” always on, independent of .devignore

Important

Your pitch is "won't leak your .env" โ€” so devbox enforces it. A built-in deny-list runs in the push path and hard-refuses to upload matched files even if .devignore is misconfigured. Belt and suspenders. ๐Ÿฉฒ

Default-blocked: .env ยท .env.* (except .env.example) ยท *.pem ยท *.key ยท id_rsa* ยท *.p12 ยท *.pfx ยท secrets/ ยท *.kdbx ยท common cloud-cred files. Blocked files show up in devbox status. Add your own via [secrets].extra_patterns in config.toml.

โš™๏ธ config.toml โ€” per-machine tunables
[transfer]
max_kbps  = 0     # blob transfer cap (KB/s); 0 = unlimited
compress  = false # ๐ŸŒ gzip blobs BOTH ways (upload + download) โ€” turn on for devices syncing over a WAN link

[sync]
rescan_seconds  = 60    # watcher-fallback rescan cadence; raise on a huge tree, lower for snappier convergence
ignore_defaults = false # ๐Ÿ™ˆ also ignore common junk (node_modules, .git, target, dist, build, .venv, __pycache__, โ€ฆ)

[secrets]
extra_patterns = ["*.secret", "vault-*"] # additional secret-guard deny patterns

compress only gzips a chunk when it actually shrinks (incompressible blobs go raw), and the hub hashes the decompressed bytes so dedup + integrity are untouched. ๐Ÿ—œ๏ธ The WAN path is also hardened with granular timeouts, automatic retry+backoff on transient blips, and parallel blob transfer โ€” no flags, always on.


๐Ÿ•ฐ๏ธ Versioning & Deploy

gitGraph
    commit id: "S1 ๐Ÿ“ธ"
    commit id: "S2 ๐Ÿ“ธ"
    commit id: "S3 โœ๏ธ"
    commit id: "S4 ๐Ÿ“"
    commit id: "S5 ๐Ÿš€ deploy"
Loading
  • ๐Ÿ“ธ Every accepted change = an immutable snapshot (BLAKE3 of the manifest). Manifests are themselves content-addressed โ†’ 100 pushes don't store the tree map 100ร—.
  • โ†ฉ๏ธ devbox restore <snap> [path] rolls back a file or whole share (itself a new change โ†’ reversible).
  • ๐Ÿš€ devbox deploy <share> <snap> pins a mount to a snapshot by applying it without pushing a new head โ€” history stays untouched and the daemon won't drag it back to latest ([pinned]). Blue/green deploys for your /var/www boxes, basically free; re-mount to resume live sync.
  • ๐Ÿงน devbox-hub gc sweeps unreferenced chunks (refcounted).

๐Ÿ–ฅ๏ธ Cross-Platform

Capability ๐Ÿง Linux ๐ŸŽ macOS ๐ŸชŸ Windows
File watching inotify FSEvents ReadDirectoryChangesW
Atomic apply rename(2) rename(2) ReplaceFile/MoveFileEx
Hooks bash bash bash (git-bash/WSL) or .ps1โ†’pwsh
Service systemd launchd Windows Service
Static binary โœ… โœ… โœ…

๐Ÿงญ Canonical paths are forward-slash + relative (converted at the Windows boundary). Filenames illegal/colliding on an OS (foo.go vs Foo.go, aux, trailing dot) โ†’ skip + warn + surface in devbox status; the hub keeps the bytes, peers that can hold the name still get the file. Never fatal. ๐Ÿ›Ÿ


๐Ÿ” Security & Durability

Threat model: single-owner, multi-device (every enrolled device is yours). Within that, v1 went through an adversarial audit โ€” every data-loss and arbitrary-file path is closed. ๐Ÿ›ก๏ธ

Layer Protection
๐Ÿชช Device identity ed25519 keypair per device; join requires proof-of-possession (a signed challenge โ€” you can't claim a key you don't hold), and a bad request never burns the one-time token
๐ŸŽŸ๏ธ Auth bearer tokens, hashed at rest (hub stores no plaintext creds), device-revocable
๐ŸงŠ Content integrity every chunk and manifest is re-verified against its BLAKE3 hash on download โ€” a corrupt/truncated transfer or hostile hub can't write wrong bytes into your tree
๐Ÿšง Path safety hub rejects any blob key that isn't 64-hex (no ..%2f traversal โ†’ no arbitrary file read); the client refuses manifest paths that escape the mount root
๐Ÿ”‘ Secret guard case-insensitive deny-list (.ENV == .env); .env/keys/*.env/.aws/credentials never leave the machine, independent of .devignore
๐Ÿ›Ÿ Never lose a byte losing local edits become .conflict copies; ignored/guarded on-disk files are preserved before any hub overwrite; atomic writes are fsync'd (power-loss safe)
๐Ÿงน Safe GC mark-and-sweep from every live head โ€” never frees a chunk a share still needs, even if refcounts are off
๐Ÿšช DoS bounds request-body caps (256 MiB blob / 8 MiB JSON โ†’ 413) + server read/idle timeouts
๐Ÿ†” Daemon single-instance pidfile with a PID-reuse guard (start-time token) so stop never signals a stranger
๐Ÿ”ฌ how the integrity + path guards chain
flowchart LR
    P["๐Ÿ“ฅ pull"] --> G{"64-hex<br/>blob key?"}
    G -->|no| X1["๐Ÿšซ 404 โ€” no traversal"]
    G -->|yes| F["โฌ‡๏ธ fetch blob"]
    F --> H{"BLAKE3<br/>matches key?"}
    H -->|no| X2["๐Ÿšซ integrity fail"]
    H -->|yes| C{"path inside<br/>mount?"}
    C -->|no| X3["๐Ÿšซ refuse escape"]
    C -->|yes| W["โš›๏ธ atomic write + fsync"]
    style X1 fill:#5a1e1e,stroke:#ff6b6b,color:#fff
    style X2 fill:#5a1e1e,stroke:#ff6b6b,color:#fff
    style X3 fill:#5a1e1e,stroke:#ff6b6b,color:#fff
    style W fill:#1e5a2e,stroke:#51cf66,color:#fff
Loading

๐Ÿ—บ๏ธ Roadmap

๐Ÿ”ฎ Looking ahead: the full v2 design spec โ€” multi-owner teams + ACLs, client-side E2E encryption (convergent, keeps dedup), LAN peer chunk-exchange + hub HA, 3-way merge, and a TUI โ€” sequenced M8โ†’M11 by dependency. M8 foundations are landing now; M9โ€“M11 stay demand-driven (build E2E when an untrusted-hub user exists, P2P when the uplink hurts, the TUI when asked).

flowchart LR
    M6["M6 ๐Ÿ•ฐ๏ธ\nVersioning"] --> M65["M6.5 ๐Ÿšข\nDeploy"]
    M65 --> M7["M7 ๐Ÿ›ก๏ธ\nHardening\n+ M7.5/7.6 audit"]
    M7 --> M8["M8 ๐Ÿ—๏ธ\nv2 Foundations\nmigration ยท teams ยท control socket"]
    M8 --> M9["M9 ๐Ÿ”\nTrust + HA\nACL ยท E2E ยท S3"]
    M9 --> M10["M10 ๐Ÿค\nCluster + merge\nP2P ยท resolver"]
    M10 --> M11["M11 โœจ\nPolish\nTUI ยท power"]
    style M6 fill:#1e5a2e,stroke:#51cf66,color:#fff
    style M65 fill:#1e5a2e,stroke:#51cf66,color:#fff
    style M7 fill:#1e5a2e,stroke:#51cf66,color:#fff
    style M8 fill:#1e5a2e,stroke:#51cf66,color:#fff
    style M9 fill:#0d1117,stroke:#4F9CF9,color:#fff
    style M10 fill:#0d1117,stroke:#4F9CF9,color:#fff
    style M11 fill:#0d1117,stroke:#4F9CF9,color:#fff
Loading
Milestone Deliverable
โœ… M0 โ€” Skeleton ๐Ÿฆด cobra CLI, devbox join, keypair, machine config
โœ… M1 โ€” Watch + secrets ๐Ÿ‘€ fsnotify, .devignore, secret-guard, FastCDC+BLAKE3 chunking, content-addressed manifests
โœ… M2 โ€” Hub + push ๐Ÿ›ฐ๏ธ shares, join tokens, CAS, publish, HTTP upload, snapshots, bearer auth, /metrics โ€” deployed on .10, verified cross-machine
โœ… M3 โ€” Two-way sync ๐Ÿ”„ SSE event fan-out, mount, pull + atomic apply, per-file 3-way conflict copies, live start daemon โ€” two Macs sync live through the hub
โœ… M4 โ€” Read-only + bw ๐Ÿ”’ server-enforced read-only bit, sub-path mounts (mount proj/app /dir), bandwidth cap โ€” fleet-verified
โœ… M5 โ€” Hooks ๐Ÿช bash (+.ps1) lifecycle runner, env injection, 60s timeout, pre-* veto โ€” post-pull fired on a fleet node
โœ… M6 โ€” Versioning ๐Ÿ•ฐ๏ธ devbox log (full snapshot ids) / restore (revert any file) / hub gc โ€” fleet-verified
โœ… M6.5 โ€” Deploy ๐Ÿš€ devbox deploy <share> <snapshot> โ€” apply a snapshot without pushing, [pinned] mount; fleet-verified blue/green
โœ… M7 โ€” Hardening ๐Ÿ›ก๏ธ devbox doctor, stop/pidfile, hook edit/list, SSE backoff+jitter, 60s rescan fallback (survives dead/limited inotify watchers โ€” PRD risk #1), share-name guard, release builds โ€” fleet-verified
โœ… M7.5 โ€” Audit hardening ๐Ÿ” adversarial security/data-loss audit + fixes: blob-hash path-traversal blocked, download blob-integrity check, manifest-path containment, secret-guard case-insensitive + more patterns, never-clobber ignored/guarded files, GC made safe vs cross-share refcount undercount โ€” all with regression tests, race-clean, fleet-verified
โœ… M7.6 โ€” Hardening complete ๐Ÿ›ก๏ธ ๐Ÿ’ฝ fsync durability on atomic writes (power-loss safe), ๐Ÿšช request size caps + server timeouts (DoS), ๐Ÿ†” pidfile PID-reuse guard (start-time token), ๐Ÿชช join proof-of-possession (ed25519 signature, token not burned on a bad request) โ€” fanned out to parallel worktree agents, regression-tested, race-clean, fleet-verified
โœ… M8 โ€” v2 Foundations ๐Ÿ—๏ธ ๐Ÿ”‘ schema migration runner (PRAGMA user_version, transactional, VACUUM INTO backup, refuses a newer DB) ยท ๐Ÿ”ข per-(share,id) snapshots (fixes the cross-share refcount undercount; GC + head-backfill reworked) ยท ๐ŸŽ›๏ธ daemon control socket (Unix socket, HTTP/1.1, 0600) wiring devbox pause/resume + live-socket-aware status ยท ๐Ÿ‘ฅ M8a: principals + per-share roles + write enforcement (devbox-hub member/principal; legacy shares = v1, first grant flips to deny-by-default; roleโ‰ฅeditor AND the writable clamp). Both migrations verified on a copy of the real .10 hub DB (counts preserved, 3 legacy heads repaired, chains 0โ†’1โ†’2). ๐Ÿ‘€ read side (GET /v1/members + devbox members) ยท โœ‰๏ธ device-facing invites (POST /v1/invite + devbox invite, privilege attenuation via pure meta.MayGrant, self-seed on the legacyโ†’explicit flip) โ€” cross-machine fleet-verified on arm64 Pis ยท ๐Ÿ›Ÿ restore preserves uncommitted edits (never-lose-a-byte on revert). M8's justified-now scope is complete โ€” the conflict sidecar moves to M10 (only its resolver consumes it) and read-side ACL gating is M9
๐Ÿ”ฎ still ahead in M8 / genuinely v2

Landed in M8 above: the migration runner, per-(share,id) snapshots, and the control socket. Still sequenced ahead:

  • ๐Ÿ“ Conflict-copy on explicit restore/deploy (M8-3) โ€” needs base/ancestor awareness so it preserves only uncommitted edits without breaking restore-reproduces-snapshot; rides the conflict sidecar.
  • ๐Ÿ‘ฅ Principals / roles / invites + write enforcement (M8a) โ€” the membership layer E2E + P2P both need underneath them.
  • ๐Ÿ”‘ Read-side ACL + deny-by-default writes (M9) โ€” only meaningful once shares span multiple owners; the genuinely new attack surface, staged after write-side.
  • ๐Ÿ”ข snapshot_chunks edge table (M9) โ€” a derived-refcount source for E2E/P2P; deferred until a consumer exists (the migration runner makes adding it a one-liner).
๐Ÿ”ฎ v2 backlog
  • ๐Ÿค LAN peer chunk-exchange โ€” co-located nodes swap chunks directly (Syncthing-style)
  • ๐ŸŽ›๏ธ Interactive conflict resolver โ€” diff + keep-mine/theirs/both/edit
  • ๐Ÿ” Re-share / delegation (the s permission)
  • ๐Ÿงฌ Content-level 3-way text merge
  • ๐Ÿ”‹ Laptop power sanity โ€” pause-on-metered / pause-on-battery / sync windows
  • ๐Ÿ” Client-side E2E chunk encryption
  • ๐Ÿฐ Hub clustering / HA
  • ๐Ÿ–ผ๏ธ Full TUI dashboard

โš–๏ธ License & Open-Core

๐Ÿ“œ AGPLv3 core โ€” self-host free, forever. ย ยทย  ๐Ÿ’ผ Commercial license for the hosted tier.

devbox is open-core: the entire hub + clients in this repo are AGPLv3 and fully self-hostable. A future hosted version (signup, billing, provisioning) is a separate, closed control plane that wraps the same single-tenant hub โ€” designed for via clean seams (BlobStore interface, config-driven limits, /metrics) but not part of this OSS repo.

Offering devbox as a hosted service? AGPLv3 means open your changes โ€” or grab a commercial license. ๐Ÿค


๐Ÿ™Œ Contributing

โœ… v1 is feature-complete โ€” the spec documents the full design, and scripts/build-release.sh cross-compiles every platform.

  1. ๐Ÿด Fork & branch
  2. ๐Ÿงช Keep it lazy-correct (smallest diff that works, tests for non-trivial logic)
  3. ๐Ÿ“ Update the docs in the same PR โ€” they get the full flare treatment ๐Ÿ˜Ž
  4. ๐Ÿš€ Open a PR

Made with ๐Ÿ“ฆ, โ˜•, and a healthy fear of rm -rf on the wrong machine.

โ€œIt's like Dropbox, but it actually respects that you're a developer.โ€ ๐Ÿ’™

About

๐Ÿ“ฆ Dropbox for devs โ€” continuous file sync with .devignore, hooks & version history. Self-hostable Go hub. AGPLv3.

Resources

Stars

Watchers

Forks

Releases

Packages

Used by

Contributors

Languages