That's the whole relay. The operator genuinely cannot read messages — not because of policy, but because the math doesn't let them.
The honest tradeoff: the relay can see that a message was sent (metadata), even if it can't see what. For real anonymity you'd want onion-routing the fan-out. That's on the roadmap.
A few things I learned along the way
-
IndexedDB is a real database, not localStorage. Treat it like one. Versioned schema, compound index
[conversationId, timestamp], cursor-based pagination. "Load last 50 messages" is O(log n) with the right index. - Service Workers can do encrypted push. The push payload itself is ciphertext, so the push service (FCM/Mozilla) only ever sees ciphertext. The SW decrypts and displays.
- The pubkey is the identity. No username lookup table. The "Vault ID" users see is just a hash of the client's x25519 pubkey, generated in the browser. Free anonymity.
-
Don't roll your own crypto layer. I almost wrote a custom TweetNaCl wrapper. Then I realized I was reinventing
tweetnacl-util. Just use it.
What's next
- Native iOS / Android (federated, still no phone number)
- Onion routing the relay fan-out
- Per-message forward secrecy (real Double Ratchet, the Signal way)
- Independent security audit
- Open-sourcing the relay
Try it / AMA
- Live web app: pulse.moneymates.app
- Project + architecture deep-dive: getpulse.moneymates.app
If you want to:
- Tell me the relay code is wrong
- Argue X25519 vs Curve25519
- Roast the "blind relay" claim
- Share IndexedDB anti-patterns I might still have
The comments are open. AMA.