Most messaging applications rely on centralized infrastructure. Messages travel through company-owned servers, identities are managed by centralized systems, and availability depends on cloud providers.
I wanted to understand what happens when you remove all of that.
Instead of reading more documentation about distributed systems, I decided to build one.
The result was Peetopee, a proof-of-concept peer-to-peer messenger built with Bun, TypeScript, libp2p, SQLite, and WebCrypto.
The goal wasn't to build a Signal competitor. The goal was to learn how peer discovery, cryptographic identity, key exchange, session management, and encrypted messaging actually work under the hood.
The Requirements
Before writing any code, I defined a few constraints:
- No centralized server
- Persistent peer identities
- End-to-end encrypted messages
- Local message history
- Terminal-first interface
- Direct peer-to-peer communication
That gave me a surprisingly complete messaging system architecture.
Why Bun?
I chose Bun mostly because I wanted to see how far its ecosystem had matured.
A few features made it particularly attractive:
- Built-in TypeScript support
- Fast startup times
- Native SQLite support
- WebCrypto APIs
- Minimal tooling setup
Since this project involved networking, cryptography, and persistence, Bun covered most of the infrastructure I needed out of the box.
The Networking Layer
For networking, I used libp2p.
Instead of worrying about low-level socket management, libp2p provides abstractions for:
- Peer discovery
- Transport security
- Stream multiplexing
- Peer identification
Peetopee uses:
- TCP
- Noise
- Yamux
- mDNS
- Identify
Communication happens over a custom protocol:
/p2p-chat/1.0.0
Messages are serialized as JSON and exchanged over libp2p streams.
Two Identities, Not One
One design decision I particularly liked was separating network identity from application identity.
Network Identity
libp2p already uses Ed25519 keys to generate Peer IDs.
These identities are persisted so nodes remain recognizable after restarts.
Application Identity
I created a second identity using WebCrypto.
This identity handles:
- Handshake authentication
- Session establishment
- Message signing
Keeping them separate made the architecture much cleaner.
Building the Handshake
The cryptographic handshake combines several modern primitives:
- Ed25519 signatures
- P-256 ECDH
- HKDF
- AES-GCM
The process looks roughly like this:
- Exchange identities
- Verify signatures
- Exchange ephemeral keys
- Perform ECDH
- Derive session keys with HKDF
- Store the session Once complete, both peers possess the same symmetric encryption key without transmitting it directly.
Persistence
A chat application isn't very useful if it forgets everything after a restart.
Peetopee stores:
- Identities
- Prekeys
- Sessions
- Messages
in SQLite.
The storage layer is intentionally boring, and that's a compliment.
SQLite provides reliability without adding unnecessary complexity.
Challenges
The hardest part wasn't networking.
It wasn't cryptography.
It was ecosystem compatibility.
Bun has come a long way, but many packages still assume a Node.js runtime. Building around those assumptions required more effort than expected.
Another challenge was designing a handshake that was educational while remaining understandable. I intentionally avoided implementing something as complex as Signal's Double Ratchet because the goal was learning, not protocol innovation.
What I Learned
Three things stood out:
libp2p removes an enormous amount of complexity from distributed systems.
Identity management is often more important than message transport.
Good architecture matters even in small projects.
Separating crypto, networking, storage, and services early prevented the project from turning into a tangled mess later.
What's Next?
Potential future improvements include:
- Double Ratchet support
- NAT traversal
- Group messaging
- Multi-device synchronization
- Better peer discovery
- Rich terminal UI
For now, though, Peetopee accomplished its original goal: helping me understand how decentralized messaging systems actually work.
The full source code is available on GitHub if you'd like to explore the implementation yourself.
Top comments (0)