← Projects

Ferry

2026

Open source · Apache-2.0

An open-source peer-to-peer tool for developer teams, Apache-2.0. Every machine runs the same daemon — client and server at once — and a CLI, a VS Code extension and a Tauri desktop app all talk to it over a local socket. Seven Rust crates in one Cargo workspace, with the dependency direction enforced by the crate graph, so breaking it is a build error rather than something a reviewer has to catch. Two daemons run the full pipeline end to end: discovery, pairing, signed rosters, encrypted transfer, delivery.

Role — Sole author — architecture, pre-implementation spec, full Rust implementation

Rust crates
7
Tests
350+ Rust
Surfaces
CLI · VS Code · Desktop
Generated bindings
37 types
Crypto primitives
4 audited
Docker pulls
546
GitHub release downloads
9
RustNoise XKSPAKE2ageed25519SQLitemDNSTauriReactts-rs

The constraint that shaped it

There is no trusted third party. Every rule has to be enforced by the machine that cares about it, against cryptographic evidence rather than a server's word. Most architecture work assumes the opposite: a server that enforces a rule once, for everyone.

The architecture

daemon4,117 linescli785 linescore6,120 linesnet609 linesstore1,517 linescrypto1,046 linesproto451 linesproto = types + error codes only, zero logic. TS bindings generated, never transcribed.
Seven crates in the spine, one direction; an eighth, remote, sits behind the off-by-default GitHub features. The Rust crate graph enforces the layering — a violation is a build error, not a code-review habit. net can't import crypto, so the roster check enters as an injected predicate over raw key bytes: networking enforces it mechanically, the domain layer supplies the decision.

What it looks like

3 screens · click to enlarge
Send a file, a secret or a message to one peer. The summary states the TTL, whether it burns on the first open, and that it's sealed with age over Noise XK. The CLI, the VS Code extension and the desktop app all render this same UI.

The constraint that shapes everything

There's no trusted third party. A few things follow from that:

An unverified peer can do nothing. Deny-by-default, enforced at the Noise handshake before any frame is parsed as a command — never in the UI. Discovery isn't trust; a hostname on a network is an unauthenticated claim. Pairing needs an out-of-band short code and a verification phrase a human confirms, and it can't be skipped.

Sending is a local write, not a network call. The outbox row is written in the same transaction as the state change, and a dispatcher drains it when the peer is reachable. It's a familiar pattern used differently: usually an outbox exists so a failed side effect can't roll back a write, but here the peer being offline is the normal case, so it's the core abstraction rather than a safety net.

Expiry runs on the receiver's monotonic clock, starting at delivery. Two laptops on a hotspot disagree about the time, and the sender's clock is attacker-controlled. A ten-minute secret that sat queued for twenty minutes still gets its ten readable minutes once it lands.

Crypto is assembled from audited primitives, never designed. age, Noise XK, SPAKE2, ed25519. No novel constructions and no hand-rolled key derivation.

The compiler owns the layering

Dependency direction: daemon/cli → core → net/store/crypto → proto. Never the reverse. core can't import the daemon, an IPC type or anything UI-related, because it has to be drivable from the daemon, the CLI or a test harness with a fake network without changing.

The networking crate can't import the crypto crate, so the roster check arrives as an injected predicate over raw key bytes. Networking enforces it mechanically and never enters transport mode without a pass; the domain layer supplies the actual roster-backed decision. Neither layer can accidentally be changed into trusting the other.

proto holds types, enums and error codes only — no logic. The 37 TypeScript binding types are generated from it with ts-rs rather than hand-transcribed, and CI fails the build on any diff.

Three surfaces, one local daemon

The daemon is the only thing that speaks the peer-to-peer protocol. Everything a human touches — the CLI, a VS Code extension, and a Tauri desktop shell over a 22-component React UI — talks to it over a length-framed local socket, and never to another machine directly.

That's why pairing lives in the CLI rather than the daemon. It needs a human to read a verification phrase aloud, and the daemon is headless with no guaranteed terminal. A step that requires a person can't live in a process that can't reach one.

The test bar is a docker compose harness that runs two daemons on a network and drives a genuine send, disconnect and reconnect between them. An in-memory two-node test tells me about the state machine; this tells me about a hotspot with a sleeping laptop on it.

How it was built

  • 24 August 2026

    Ferry — the core lands

    Written against a specification finished before any code. The workspace, the protocol types, crypto, networking, storage, the transfer engine and outbox, and the daemon skeleton.

  • 27 August 2026

    Ferry speaks to other machines

    A real listener accepting Noise XK connections authorised against the roster, mDNS discovery, and SPAKE2 pairing with human confirmation.

Problems that came out of this project9

Related notes