A production-ready, forkable starter template for real-time encrypted group chat. Built for teams and developers who need a self-hosted messaging baseline with strong security defaults and zero frontend framework dependencies.
Remix this project on Replit — fork and deploy in one click, no local setup required. New to Replit? Create a free account.
- End-to-end encryption — Messages are encrypted in the browser with AES-256-GCM before being sent. The server never sees plaintext. Each room has its own key, delivered to the client via ECDH P-256 key exchange.
- Room keys encrypted at rest — The server stores room keys wrapped with AES-256-GCM using a key derived from
MASTER_ENCRYPTION_KEYvia PBKDF2-SHA512 (600,000 iterations, per-key random salt). - Replit OIDC authentication — Session-based login using Replit's built-in OpenID Connect provider with PKCE. No client credentials required when deployed on Replit. Sessions stored in PostgreSQL and expire after 24 hours.
- Public and private rooms — Anyone can join public rooms. Private rooms require an invite link created by the room creator. Invite links have configurable expiry (default 24 hours) and a maximum number of uses (default 10).
- WebRTC signaling relay — The server relays SDP offers, answers, and ICE candidates between peers for establishing direct connections. The server acts as a blind relay and cannot inspect WebRTC traffic.
- Real-time lobby and presence — A lobby panel shows all currently connected users with live updates. Per-room member lists update in real time. Presence is multi-device aware: a user only goes offline when all of their sockets disconnect.
- Client-side shadow-ban profanity filter — Runs in the sender's browser before encryption. Flagged messages are echoed locally so the sender believes delivery occurred, but the encrypted payload is never transmitted. The server cannot perform this check because it never sees plaintext.
- Structured rate limiting — Separate rate limiters for room joins, message sends, history requests, invite creation, and invite use. Limits are applied per user ID or IP depending on the operation.
| Layer | Technology |
|---|---|
| Runtime | Node.js ≥ 20 |
| HTTP server | Express 4 |
| Real-time transport | Socket.IO 4 |
| Database | PostgreSQL (via pg) |
| Session storage | connect-pg-simple |
| Authentication | Replit OIDC via openid-client |
| Encryption (server) | Node.js crypto — AES-256-GCM, PBKDF2-SHA512 |
| Encryption (client) | Web Crypto API — AES-256-GCM, ECDH P-256 |
| Input validation | Zod |
| Security headers | Helmet |
| Logging | Pino (JSON); pino-pretty for local development |
| Frontend | Vanilla HTML / CSS / JavaScript — no framework |
- Node.js v20 or higher
- PostgreSQL 14 or higher
- A Replit account (required for the built-in OIDC provider)
pnpmv8 or higher (this project is a pnpm workspace)
-
Fork or clone this repository:
git clone https://github.com/leejaew/template-secure-chat.git cd template-secure-chat -
Install dependencies from the workspace root:
pnpm install
-
Copy the example environment file and fill in all values:
cp artifacts/securechat/.env.example artifacts/securechat/.env
-
Create the database schema. Connect to your PostgreSQL instance and run:
psql $DATABASE_URL -f artifacts/securechat/src/db/schema.sql -
Generate the required secrets and add them to
.env:# SESSION_SECRET — at least 32 characters openssl rand -base64 48 # MASTER_ENCRYPTION_KEY — exactly 64 hex characters openssl rand -hex 32
Important:
MASTER_ENCRYPTION_KEYmust remain stable across restarts. Changing it after rooms have been created makes all stored room keys unrecoverable and breaks message history.
pnpm --filter @workspace/securechat run devThe server starts on the port specified by PORT (default 3000). Open http://localhost:3000 in a browser. A Pino log line like {"level":30,"msg":"Server listening","port":3000} confirms a successful start.
| Variable | Description | Required |
|---|---|---|
NODE_ENV |
Set to production to enable HSTS, secure cookies, and suppress stack traces. |
Optional (defaults to development) |
PORT |
Port the HTTP server listens on. | Optional (defaults to 3000) |
APP_URL |
Full public URL of this deployment — e.g. https://your-app.replit.app. Used as the OIDC callback base and for generating invite links. |
Required |
REPLIT_OIDC_ISSUER |
OIDC discovery endpoint. Change only if using a custom provider. | Optional (defaults to https://replit.com/oidc) |
SESSION_SECRET |
Signs and verifies session cookies. Must be at least 32 characters. Generate with openssl rand -base64 48. |
Required |
DATABASE_URL |
PostgreSQL connection string — e.g. postgresql://user:password@localhost:5432/securechat. |
Required |
MASTER_ENCRYPTION_KEY |
64-character hex string used to wrap room keys at rest. Generate with openssl rand -hex 32. Never change this after rooms exist. |
Required |
Why the server never filters messages: All message content is encrypted client-side before transmission. The server stores and forwards ciphertext only. The profanity filter in public/js/profanity.js therefore runs in the sender's browser before the message is encrypted — this is the only point in the pipeline where plaintext is accessible.
Key exchange flow: When a client joins a room, it generates an ephemeral ECDH P-256 key pair and sends the public key to the server. The server generates its own ephemeral pair, derives a shared secret, uses it to wrap the room's AES-256-GCM key, and sends the wrapped key back alongside its public key. The client unwraps the room key locally. The raw room key never travels over the network.
Room key storage: Room keys are wrapped with AES-256-GCM using a key derived from MASTER_ENCRYPTION_KEY via PBKDF2-SHA512 with 600,000 iterations and a per-key random 32-byte salt. The encrypted key, IV, auth tag, and salt are stored in PostgreSQL. The plaintext room key exists in server memory only during a key exchange operation.
MIT