All message content is encrypted with AES-256-GCM before being written to the database:
A random 12-byte IV is generated per message
The plaintext is encrypted: AES-256-GCM(key, iv, plaintext)
The IV, authentication tag, and ciphertext are concatenated and base64-encoded
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.