architecture

Stack overview

Client (Browser)

    │ HTTPS / WSS (TLS 1.3)

Nginx (reverse proxy, SSL termination)


Fastify (Node.js, port 3030)
    ├── PostgreSQL  (Upgraded chat storage)
    └── Redis       (cache + pub/sub + encrypted message storage)

Encryption

Messages at rest

All message content is encrypted with AES-256-GCM before being written to the database:

  1. A random 12-byte IV is generated per message

  2. The plaintext is encrypted: AES-256-GCM(key, iv, plaintext)

  3. The IV, authentication tag, and ciphertext are concatenated and base64-encoded

  4. Only the encoded ciphertext is stored in the database

The encryption key is a 64-character hex string stored in the server's environment variables. It is never transmitted to clients and never logged.

Messages in transit

All traffic between clients and the server uses TLS 1.3 (enforced by Nginx). WebSocket connections use wss:// — unencrypted ws:// connections are rejected.


IP address handling

The salt is a server-side secret stored in environment variables. The hash cannot be reversed.


WebSocket security

  • Only wss:// connections are accepted (enforced by Nginx)

  • Origin validation: only https:// origins are accepted; plain http:// origins are rejected

  • Message schema validation: every incoming WebSocket message is validated with Zod before processing

  • No eval(), no new Function(), no dynamic code execution


Content Security Policy

Different CSP policies are applied per route type:

Route type
Policy

API endpoints

default-src 'none' — nothing is allowed

Widget HTML pages

Full CSP with whitelisted CDNs, frame-ancestors *

Other HTML pages

Full CSP, frame-ancestors 'none'

WebSocket routes

No CSP applied (upgrade requests)


Rate limiting

Action
Limit

Messages per token per IP

100 per 24 hours

Votes per token per IP

1 per 24 hours

Room creation per IP

10 per 24 hours

Message reactions

30 per minute


DOMPurify

All user-generated content is sanitised with DOMPurify before rendering in the browser. The configuration is strict:

This means no HTML tags, no links, no images, and no scripts can survive in a message — only plain text.


XSS protection

In addition to DOMPurify, all user content is passed through an escH() function that HTML-encodes &, <, >, and " before insertion into the DOM. This is a defence-in-depth measure on top of DOMPurify.


No eval, no dynamic code

The widget and chat codebases contain no:

  • eval()

  • new Function()

  • innerHTML with unescaped user content

  • Dynamic <script> injection

Last updated

Was this helpful?