Skip to content

Engineering case study · A project by Jason Harmon

Local-first messaging. Explicit engineering tradeoffs.

EmberChamber brings native clients, a web companion and an edge-hosted delivery system together around invite-only conversations. This is a guided map of the implementation and its boundaries, not a claim that every client has feature parity.

Development is AI-assisted. Use the links below to review architecture notes, source changes, repository guidance and automated workflow results.

System overview

Three responsibilities, kept distinct.

  1. 01 · Clients

    Interface and local state

    Next.js for the browser, React Native / Expo for mobile, and a Tauri desktop shell. Client-specific storage and key handling remain part of the security boundary.

    apps/web · apps/mobile · apps/desktop

  2. 02 · Relay

    Routing and coordination

    Cloudflare Workers handle relay APIs. Durable Objects coordinate per-device mailboxes, groups and rate limits; queues support background work.

    apps/relay · Workers · Durable Objects

  3. 03 · Persistence

    Explicit storage boundaries

    D1 stores account and routing metadata; R2 stores attachment objects. The documented distinction between encrypted conversations, hosted rooms and client-local history matters.

    D1 · R2 · Device-local state

Design decisions

The reasoning behind the implementation.

Use a delivery relay, not a universal private-message archive

Per-device Durable Objects queue ciphertext envelopes, deliver them over WebSockets and remove them after acknowledgement or expiry.

Tradeoff: Offline delivery still requires infrastructure. Account, device, membership and routing metadata remain server-side; not every group or room path has the same storage boundary.

Relay implementation

Keep private-content search close to the device

Private-message history and its search index stay with the client rather than becoming a server-side full-text archive.

Tradeoff: Local-first state makes device loss, key handling and recovery important product problems. Recovering an account is not equivalent to restoring all lost history.

Storage and recovery architecture

Make cross-client protocol changes explicit

Rust and TypeScript contract definitions describe sessions, device bundles, mailbox envelopes, groups and attachments across the active runtimes.

Tradeoff: Shared definitions help reviewers track changes, but they do not replace cross-client tests. The Rust core is still partial, not the primary engine for every client flow.

TypeScript protocol package

Treat source, deployment and installers as separate evidence

The repository includes CI workflows for builds, checks and screenshots, plus a release feed for downloadable native artifacts.

Tradeoff: An implemented feature is not automatically deployed or present in a published installer. Release identity and per-client support must be checked independently.

Build and verification workflows

Work still in progress

A beta with visible boundaries.

Current source encrypts conversation attachment bytes before upload across web, mobile and desktop. Device-encrypted conversations keep file keys with participants; relay-hosted rooms and legacy groups use recoverable relay-side key material. The relay also receives exact plaintext length and a deterministic plaintext hash for encrypted uploads today. Production deployment, published-installer parity, authenticator validation and recovery maturity require separate verification.

The README also documents historical release-version mismatches. Inspect the artifact and its notes rather than assuming a newer source commit means a newer installed client.