DEV Community

nates.dev
nates.dev

Posted on

I Built a P2P Messenger Without a Central Message Database — What I Learned About WebRTC, E2EE and Serverless Architecture

I Built a P2P Messenger Without a Central Message Database — What I Learned About WebRTC, E2EE and Serverless Architecture

I wanted to answer a fairly simple question:

How much of a modern messaging application can be moved away from a traditional centralized backend?

That question eventually turned into OpenChat, an open-source P2P communication application built around WebRTC, browser-native cryptography, MQTT signaling and serverless infrastructure.

The goal was not to create a magical "unhackable" messenger or claim that decentralized architecture automatically makes an application private.

The goal was much more practical:

Build a usable real-time communication application where the central infrastructure does not store the users' conversations or act as the transport layer for normal message traffic.

This article explains the architecture, the security model, the limitations I encountered, and some of the security problems I discovered while building it.


🏗️ The Architecture

The traditional architecture for a messaging application usually looks something like this:

Client A
   |
   v
Application Server
   |
   v
Database
   |
   v
Application Server
   |
   v
Client B
Enter fullscreen mode Exit fullscreen mode

The server is responsible for authentication, message routing, storage, synchronization, presence and often many other application-level responsibilities.

OpenChat takes a different approach.

                 MQTT
             Signaling only
              /         \
             v           v
        Browser A     Browser B
             \           /
              \         /
               WebRTC
            DataChannel
Enter fullscreen mode Exit fullscreen mode

The actual communication between peers is handled by WebRTC.

MQTT is used for signaling and peer discovery. Its purpose is to help the browsers exchange the information required to establish their WebRTC connection.

Once the connection is established, application data can travel through the WebRTC DataChannel instead of passing through an application server.

The distinction is important:

MQTT handles signaling. WebRTC handles the actual peer communication.

This also means that losing the signaling connection after a successful WebRTC session does not necessarily mean that the existing DataChannel immediately stops carrying data.


☁️ What Does "Serverless" Actually Mean Here?

One of the things I learned while building this project is that the word "serverless" can be misleading.

OpenChat still uses serverless functions.

Those functions handle operations such as configuration and connection-related information.

There is also external infrastructure involved in signaling and, depending on the network, TURN relay.

So a more accurate description is:

The application has no central message database or traditional application server responsible for routing normal chat traffic.

That's more useful than simply saying "there are no servers."

Architecture diagrams should describe what the infrastructure actually does rather than what sounds impressive.


🌐 Why WebRTC?

WebRTC provides something that is extremely useful for this architecture:

browser-to-browser communication.

It can establish connections between browsers even when both peers are behind NAT, although the exact connectivity depends on the network environment.

The connection process roughly involves:

  1. Discovering the other peer.
  2. Exchanging signaling information.
  3. Gathering ICE candidates.
  4. Performing connectivity checks.
  5. Establishing the WebRTC connection.
  6. Opening the DataChannel.
  7. Moving application data through the peer connection.

The difficult part is that browsers generally cannot simply connect directly to each other without first exchanging information.

That's where signaling comes in.


📡 MQTT as the Signaling Layer

I chose MQTT for signaling because it provides a convenient asynchronous communication mechanism between clients.

The important architectural boundary is:

MQTT is not the message transport.

It helps the peers find and negotiate with each other.

After the WebRTC connection has been established, the messaging layer uses the peer connection.

This gives the system a useful separation:

Signaling layer
      |
      v
    MQTT
      |
      v
WebRTC negotiation
      |
      v
P2P connection
      |
      v
Application data
Enter fullscreen mode Exit fullscreen mode

🔐 End-to-End Encryption

P2P communication by itself does not automatically equal end-to-end encryption.

This was another important distinction during development.

WebRTC already provides transport security, but I wanted the application layer to have its own cryptographic model rather than treating the transport protocol as the entire security boundary.

OpenChat uses browser-native cryptographic APIs for operations including:

  • AES-GCM encryption
  • ECDH P-256 key agreement
  • Ed25519 / ECDSA-P256 signatures
  • PBKDF2-SHA-256
  • Cryptographically secure random number generation

The browser generates and handles the relevant cryptographic material.

The basic conceptual model is:

Plaintext
    |
    v
Application encryption
    |
    v
Encrypted message
    |
    v
WebRTC DataChannel
    |
    v
Encrypted message
    |
    v
Application decryption
    |
    v
Plaintext
Enter fullscreen mode Exit fullscreen mode

The two layers have different purposes:

WebRTC protects the transport.

Application-level cryptography protects the application data model.


🪪 Identity Is Harder Than Encryption

This turned out to be one of the most interesting security problems.

Encryption can answer:

"Can someone without the appropriate key decrypt this?"

Identity asks a different question:

"How do I know that this public key actually belongs to the person I think I'm talking to?"

A decentralized application without a central identity authority cannot magically solve that problem.

Suppose Alice contacts Bob for the first time.

Alice can establish a cryptographic relationship with the peer and verify future messages against that identity.

But how does Alice know that the key belongs to the real Bob?

There is no central authority in the architecture that can answer that question.

This is why OpenChat uses persistent cryptographic identities and fingerprint verification.

For an important conversation, the identity fingerprint can be compared through another trusted channel.

The limitation is intentional and documented:

The application cannot independently verify the real-world identity of a first-time contact.

This is not something that adding another JavaScript function magically fixes.

It is a consequence of the trust model.


🛡️ The Security Review Was Almost More Interesting Than the Original Development

After getting the main architecture working, I started looking at it from the perspective of an attacker rather than a developer.

That changed the project considerably.

Several issues appeared that were not obvious from simply looking at whether messages were encrypted.

🔴 Peer Impersonation

One of the most important problems involved trusting identity information supplied by the peer.

A field such as:

from: "user123"
Enter fullscreen mode Exit fullscreen mode

doesn't prove that the packet actually originated from the cryptographic identity associated with user123.

The application needed to connect the claimed identity to the cryptographic state of the connection.

This led to additional identity ownership and key verification checks.


🔄 Replay Attacks

Another problem was replay.

An attacker doesn't always need to create a new valid message.

Sometimes they can simply capture a previously valid application message and attempt to make the recipient process it again.

This becomes particularly interesting for state-changing operations.

For example:

group_update
group_kick
message_edit
message_delete
Enter fullscreen mode Exit fullscreen mode

A normal message ID deduplication mechanism is not necessarily enough for all of these operations.

A previously valid state-changing message can have different consequences from a duplicate chat message.

The solution involved tracking freshness and previously applied operations where appropriate.

During the review I also found that message editing needed its own replay protection rather than relying on the message-ID deduplication used for ordinary messages.

That was a good example of why security reviews need to follow the actual code paths rather than just checking whether some generic "deduplication" exists somewhere in the application.


👥 Group Authorization

Another issue involved group membership.

A user could have an existing DataChannel connection even after their membership state changed.

That creates an important distinction:

Having a connection is not the same as having permission.

The application therefore needs to validate authorization when processing group operations rather than assuming that an established WebRTC connection automatically grants continued access.

This resulted in explicit membership checks for relevant operations.


🚫 Fail-Closed Cryptography

One rule I became increasingly strict about was:

If cryptographic verification or encryption fails, don't silently continue with plaintext.

A dangerous pattern in security-sensitive software is:

try encryption
if encryption fails:
    send plaintext
Enter fullscreen mode Exit fullscreen mode

That may make the application appear more reliable, but it silently destroys the security guarantee.

The safer behavior is to fail the operation and inform the user.

This principle was applied to message encryption and local encrypted storage as well.

Reliability is important, but silently violating the security model is worse than showing an error.


💾 Local Storage Is Part of the Threat Model

It is easy to focus entirely on the network when building an E2EE application.

But the browser itself is part of the security boundary.

OpenChat uses encrypted local storage mechanisms for information that needs to persist locally.

This creates another question:

What happens if encryption fails?

The answer should not be:

"Just store the plaintext instead."

Local storage therefore follows the same fail-closed principle.

The project also clears relevant temporary state when sessions end.


🔑 API Secrets and the Serverless Boundary

At one point, a configuration endpoint exposed more information to the browser than it actually needed.

This was an important architectural lesson.

If a client only needs the result of a server-side computation, the client does not necessarily need the secret used to perform that computation.

For example, OpenChat used a secret to derive a rotating signaling topic.

Originally, the client received the secret and performed the derivation.

That wasn't necessary.

The architecture was changed so that the serverless function performs the HMAC derivation and sends the resulting topic to the client.

Before

Server
  |
  | secret
  v
Browser
  |
  | HMAC(secret, data)
  v
Topic
Enter fullscreen mode Exit fullscreen mode

After

Server
  |
  | HMAC(secret, data)
  v
Browser
  |
  | already-derived topic
  v
MQTT
Enter fullscreen mode Exit fullscreen mode

The P2P architecture remains unchanged, but the secret no longer needs to cross the server-to-client boundary.

This is a good general rule:

Don't send a secret to a client merely because the client can perform a calculation with it.


🧱 Security Headers Still Matter

A P2P architecture doesn't make browser security irrelevant.

The application still has a normal web attack surface.

That includes:

  • XSS
  • DOM injection
  • unsafe URL schemes
  • clickjacking
  • content-type confusion
  • excessive browser permissions
  • insecure resource loading

OpenChat therefore uses browser-side input sanitization and security headers including a Content Security Policy and other HTTP security headers.

One important lesson here was that CSP should be treated as a defense layer rather than a magical guarantee that XSS is impossible.

The application still needs correct output encoding and safe DOM handling.


💥 Resource Exhaustion

P2P does not automatically eliminate denial-of-service problems.

In fact, some resource-exhaustion problems move directly onto the receiving device.

A malicious peer could potentially send unexpectedly large or malformed data.

The application therefore validates incoming DataChannel messages and applies limits to things such as message and file sizes.

This is especially important because the browser is now doing work that a traditional backend might otherwise have performed.

The client is not just a UI anymore. It is part of the communication infrastructure.


🔄 NAT Traversal and TURN

Direct P2P connectivity isn't always possible.

NAT configurations, firewalls and restrictive networks can prevent two peers from establishing a direct path.

That's where STUN and TURN become important.

A simplified model is:

        Direct connection

Peer A -------------------- Peer B

              |
              | if unavailable
              v

          TURN relay
         /           \
     Peer A         Peer B
Enter fullscreen mode Exit fullscreen mode

This creates another important privacy distinction:

P2P does not mean that an intermediary can never relay traffic.

A TURN server may be involved when direct connectivity fails.

The application-level encryption model therefore shouldn't depend on the assumption that every packet always travels directly from one device to another.


⚖️ What I Would Not Claim

After working on the project, I became much more careful with security claims.

I would not describe OpenChat as:

  • ❌ Unbreakable
  • ❌ Completely anonymous
  • ❌ Immune to metadata analysis
  • ❌ Automatically secure because it is P2P
  • ❌ Equivalent to a professionally audited secure messenger

Those claims would be misleading.

There are still architectural limitations.

📭 No Central Offline Queue

If both users aren't available at the same time, there is no central message database waiting to deliver everything later.

📱 Mobile Background Limitations

Mobile browsers can suspend or terminate background activity, which can interrupt P2P communication and notifications.

🪪 First-Contact Identity

The application cannot independently establish that a first-time peer corresponds to a specific real-world person.

🕵️ Metadata

Removing a central chat database does not mean that all metadata disappears.

Signaling infrastructure and network infrastructure can still process connection-related information.

💻 Client Compromise

If the user's device or browser environment is compromised, application-level encryption cannot magically protect the plaintext after it reaches the endpoint.

These limitations are part of the threat model.


🧠 What I Learned

The biggest lesson wasn't actually WebRTC.

It was this:

Architecture changes the security problems rather than eliminating them.

A centralized application gives you problems such as:

Database compromise
Server compromise
Central authentication
Central authorization
Message retention
Server-side abuse
Enter fullscreen mode Exit fullscreen mode

A P2P architecture removes or reduces some of those problems.

But it introduces or amplifies others:

Peer authentication
Identity verification
Client-side authorization
Untrusted input from peers
Resource exhaustion on endpoints
NAT traversal
Signaling security
Device compromise
Enter fullscreen mode Exit fullscreen mode

There is no architecture where security problems disappear.

They move.


🚀 OpenChat Today

OpenChat is currently an open-source project built around this architecture.

The source code is available on GitHub, and there is also a live browser demo.

Source Code

GitHub:
https://github.com/nateS670/OpenChat

Live Demo

OpenChat:
https://openchatt.vercel.app

The project is released under the MIT License.

I'm particularly interested in feedback from people who have experience with:

  • WebRTC
  • Browser security
  • Applied cryptography
  • E2EE protocol design
  • MQTT
  • P2P systems
  • NAT traversal
  • Threat modeling

The most useful feedback isn't:

"This looks cool."

It's:

"Here's where I think your security assumption is wrong."

That's exactly the kind of feedback that has already made the project substantially better.


💭 Final Thought

I originally started OpenChat because I wanted to experiment with P2P communication in a real application.

What started as a WebRTC project eventually became an exercise in understanding trust boundaries.

The most important realization was that encryption is only one part of a secure communication system.

You also have to ask:

Who are you talking to?

Who is allowed to perform this action?

Can an old valid message be replayed?

What happens when verification fails?

What information actually needs to leave the server?

What happens when the peer itself is malicious?

And perhaps most importantly:

What assumptions am I making that the code never actually verifies?

Those questions ended up shaping OpenChat much more than the original decision to use WebRTC.

I'm still working on it, and I'm still expecting people to find things I've missed.

Top comments (0)