What's end-to-end encrypted. What isn't. No marketing.
Reach uses standard WebRTC for the actual call. Your voice is end-to-end encrypted between the two browsers — we can't decrypt it even if we wanted to. Metadata (who called whom, when, for how long) is server-mediated. This page spells out exactly what means, layer by layer, so there's no marketing confusion.
The layers, in plain English
packages/reach-signaling-cf/PROTOCOL.md commits to never logging SDP or ICE candidate content (they contain local network info that would reveal your network topology). Workers tail logs include only event types and call IDs, not bodies.
@alice to @bob requires our server to know both handles. The call_log table on the operator dashboard records caller hash, callee hash, started_at, duration, decision (answered / voicemail / declined). It does not record audio or transcript. Compared to Signal, which uses "sealed sender" to hide caller identity from the server — we don't have that yet. It's a real architectural addition planned for a future version.
Where keys live
- SRTP media keys — negotiated per-call between the two browsers via DTLS handshake inside the WebRTC PeerConnection. Discarded when the call ends. Never persisted, never exported.
- HANDLES_HMAC_KEY (32-byte hex) — Cloudflare Worker secret. Used to HMAC-hash phone numbers + sign Reach session JWTs. No recovery path if rotated; rotation would invalidate every existing phone-binding (intentional).
- HANDLES_AES_KEY (32-byte hex) — Cloudflare Worker secret. AES-256-GCM key for encrypting phone numbers at rest in D1.
- Cloudflare Calls TURN credentials — short-lived (10 min) tokens minted per-session. Never reused. Issued via signed HMAC from our Calls App Token.
- Your wallet's private key (if you used wallet-binding) — lives in your wallet (Phantom / Solflare / Backpack / Seed Vault). Never sent to us; we only see the SIWS signature you produce.
What we never log
- The contents of your calls. Audio is end-to-end encrypted; we couldn't log it if we wanted to.
- SDP offer/answer bodies. The protocol relays them but doesn't write them to any persistent log.
- ICE candidate bodies. Same — relayed, not persisted.
- Capability arguments or outputs. The CI guard at
.github/workflows/ci.ymlfails the build if anylogRequest({ metadata: { args: ... } })pattern appears in source. Privacy-by-build, not privacy-by-promise. - Plaintext phone numbers in queryable storage. Phones live only as HMAC hash + AES-GCM ciphertext. Plaintext appears in memory only when we need to send SMS (Twilio adapter), and is discarded after the call returns.
What we can log (and what we do log)
- HTTP request lines — method, path, status, duration. Standard Cloudflare Workers tail format. Used for ops debugging; retained ~7 days at Cloudflare's default.
- Call routing events — call_id, from-handle hash, to-handle, decision, duration, outcome. Persisted in D1 for the operator analytics dashboard. NOT included for personal-tier handles by default.
- Wallet challenge attempts — nonce, pubkey, purpose, created_at, expires_at, consumed flag. Required for SIWS replay protection. Pruned after 24 hours.
- SMS adapter delivery confirmations — Twilio / console adapter logs the recipient phone (hashed only — Twilio gets plaintext because Twilio needs it to deliver). We don't store the plaintext.
Compared to Signal
Signal is the gold standard. We are not Signal. Here's the precise gap:
/handles/:h + bloom filterIf you need Signal-grade metadata privacy today, use Signal. If you want a free, brandable, callable handle that rings your browser — with the same encrypted-voice guarantee you get from a Google Meet 1:1 — Reach is appropriate. Closing the metadata-privacy gap is a real architectural project we'd love to do; it's tracked in our backlog as "sealed-sender + PIR contact discovery."
What this is not
- Not telephone service. Reach does not route to 911 / 000 / 112. Don't share your handle as an emergency contact. See Terms §5.1.1.
- Not anonymous. We see who called whom. If that's a problem for your threat model, use a tool designed for anonymity (Signal, or Tor-onion-routed XMPP).
- Not jurisdictionally arbitraged. Our servers run on Cloudflare (US + EU edges). We are subject to US / AU / EU legal process. We will resist overbroad requests but cannot refuse valid ones.
- Not formally audited. No third-party penetration test as of v0.1. The code is open source (github.com/inferlane/reach); we welcome scrutiny.
Reproducing the claim
Want to verify "voice is E2E encrypted" yourself rather than take our word for it? In Chrome:
- Open
chrome://webrtc-internals/in a tab. - Place a call from
reach.inferlane.dev/@whoever. - Find the active
RTCPeerConnection; check theiceConnectionStateand theSctpTransport'sDtlsTransport— you'll seeDTLS state: connectedwith a fingerprint of the remote peer's certificate. - Confirm the chosen ICE candidate pair: if
local-candidate typeandremote-candidate typeare bothhostorsrflx, media is direct browser-to-browser. Ifrelay, it's going through TURN (still encrypted; just relayed). - Inspect the
SrtpEncoderstats — you'll seecipherSuite: AEAD_AES_128_GCM(or similar). Those are the keys negotiated directly between you and your peer.
The source for the signaling server is in packages/reach-signaling-cf/. The auth + verify flow for SIWS wallets is in packages/reach-handles/src/wallet/. Read it. File issues for anything you find sketchy.