End-to-end encrypted messaging — self-hosted, open source, built for privacy.
SecureChat is a self-hosted messaging app where every message, voice clip, and file is encrypted on your device before it ever leaves. The server stores only ciphertext — nobody but you and your contacts can read your conversations, not even the server operator.
Run it on your own server in minutes with Docker. Invite people via time-limited links, manage users from the admin panel, and install it as a PWA on any device.
- 🔒 End-to-end encryption — RSA-OAEP 2048-bit key exchange + AES-256-GCM per-message encryption
- 💬 Real-time messaging — instant delivery via Socket.IO
- 🎤 Encrypted voice messages — record and send voice clips with duration display; encrypted before upload
- 🖼️ Encrypted file & image sharing — files encrypted client-side, auth-protected downloads, full-screen lightbox
- 📋 Paste to attach — paste an image from clipboard directly into the chat input
- ↩️ Replies, reactions, edit & delete — full message management; reaction picker stays open for multi-react
- 📱 Swipe to reply — swipe right on any message on mobile
- ✅ Delivery receipts — sent, delivered, and read tick states; group chats show "Seen by X of Y"
- 🔍 Message search — search by sender or attachment filename with jump-to-message
- ⏱️ Disappearing messages — per-conversation auto-delete timer (1 hour, 24 hours, 7 days, 30 days)
- 👥 Group chats — admin controls, member management, promote/demote admins, group description, online member count
- 📨 Group invitations — invite users with an accept/decline flow; blocked users cannot be invited
- 🔕 Conversation muting — suppress push notifications per conversation with optional expiry
- 📦 Conversation archive — hide conversations without deleting; auto-unarchives on new message
- 🚫 User blocking — block users from messaging you; manage from profile settings
- 💬 Unread jump button — shows unread count, jumps to first unread message
- 🔔 Push notifications — web push on desktop and iOS (16.4+)
- 📧 Email notifications — login alerts, password changes, account deletion (via Resend)
- 👁️ Last seen timestamps — shows when a contact was last online; hide your own with the privacy toggle
- ⌨️ Typing indicators — real-time per-conversation typing state
- 🔐 Two-factor authentication — TOTP with recovery codes, trusted devices for 30 days, and admin reset
- 🔑 Password reset via email — self-serve reset link with 1-hour expiry
- 🗑️ Account self-deletion — permanently deletes your account and all messages (GDPR compliant)
- 🙈 Hide Last Seen — privacy toggle to prevent contacts from seeing your last online time
- 🔑 Password-protected key backup — keys wrapped with your password; restore automatically on any device
- 🌙 Light/dark mode — per-user preference saved locally
- 📱 PWA — installable on iOS (Safari → Share → Add to Home Screen) and Android (Chrome → Install App)
- 🛡️ Admin panel — user management, suspend/delete/password reset/2FA reset, usage stats, pagination and search
- 📋 Audit log — full timestamped log of admin actions with filters and pagination
- 🔗 Invite links — time-limited single-use links with optional email delivery; invite banner shown on auth page
- 🔒 Registration control — open or close new user registration from the admin panel without redeploying
- ✉️ Email verification — admin-controlled; admins can resend or manually verify users
- 🔐 Let's Encrypt SSL — automatic HTTPS via the setup script
- On register — a 2048-bit RSA-OAEP keypair is generated in your browser using the Web Crypto API
- Key wrapping — your private key is encrypted with an AES-256-GCM key derived from your password via PBKDF2 (100,000 iterations)
- Server storage — only the encrypted private key is stored on the server; it is useless without your password
- On login — your encrypted key material is fetched and unwrapped locally using your password
- Sending a message — a fresh AES-256-GCM key is generated per message, used to encrypt the content, then the key is wrapped with each recipient's RSA public key
- File attachments — encrypted client-side with a per-file AES-256-GCM key before uploading
Your private key never leaves your device in plaintext. The server cannot read your messages or files.
| Layer | Technology |
|---|---|
| Frontend | React 18 + Vite, Socket.IO client, Web Crypto API |
| Backend | Node.js + Express, Socket.IO, Pino structured logging |
| Database | MongoDB |
| Cache / Presence | Redis |
| Proxy | Nginx (HTTPS, CSP headers) |
| Infrastructure | Docker + Docker Compose |
- A Linux server (Ubuntu 22.04+ recommended)
- Docker and Docker Compose installed
- A domain name pointing to your server (optional, but required for Let's Encrypt SSL and iOS push notifications)
git clone https://github.com/roman-parish/securechat.git
cd securechat
./setup.shFor HTTPS with automatic Let's Encrypt SSL (recommended for production):
./setup.sh --domain=yourdomain.comsetup.sh handles everything automatically:
- Generates secure JWT secrets, MongoDB password, and Redis password
- Generates VAPID keys for push notifications
- Issues a TLS certificate (Let's Encrypt if
--domainis set, self-signed otherwise) - Builds and starts all containers
Self-signed cert: Without
--domain, your browser will show a certificate warning. Click Advanced → Proceed to continue. Push notifications and PWA installation require a real domain with a valid cert.
- Open the app in your browser and register your account — the first user to register is not automatically an admin; you set this yourself
- SSH into your server and open
.env:nano ~/securechat/.env - Set
ADMIN_USERNAMESto your username:ADMIN_USERNAMES=yourusername
- Restart the backend to pick up the change:
docker compose up -d backend
- Reload the app — you'll see an Admin button in the sidebar
With registration closed by default, you invite people via time-limited links:
- Open the Admin panel → Settings tab
- Under Registration & Access, generate an invite link
- Share the link — recipients see a banner showing who invited them when they open it
- Or set
registrationOpen: truein the admin panel to allow anyone to register
These are optional but unlock password reset, login alerts, and push notifications.
Email (via Resend):
RESEND_API_KEY=re_your_api_key_here
EMAIL_FROM=SecureChat <noreply@yourdomain.com>Required for: password reset, login alerts, 2FA notifications, invite link email delivery.
Push notifications:
VAPID_PUBLIC_KEY=...
VAPID_PRIVATE_KEY=...
VAPID_EMAIL=admin@yourdomain.comsetup.sh generates VAPID keys automatically. Push notifications require HTTPS with a valid domain (not a self-signed cert) — this is a browser requirement, not a SecureChat limitation.
After updating .env:
docker compose up -d backendgit pull && ./deploy.shdeploy.sh pulls the latest code, rebuilds containers, and restarts services with zero-downtime rolling restarts.
The included workflow (.github/workflows/deploy.yml) runs the full test suite and deploys to your server on every push to main.
Add these secrets to your GitHub repository under Settings → Secrets and variables → Actions:
| Secret | Value |
|---|---|
SERVER_HOST |
Your server's IP address or domain |
SERVER_USER |
SSH username (e.g. root or ubuntu) |
SERVER_SSH_KEY |
Contents of an SSH private key with access to the server |
The server must have the repo cloned at ~/securechat and a valid .env in place before the first deploy runs.
Copy .env.example to .env and fill in your values.
# ── Database ────────────────────────────────────────────────────────────────
MONGO_USER=admin
MONGO_PASSWORD=change_this_mongo_password
# ── Redis ───────────────────────────────────────────────────────────────────
REDIS_PASSWORD=change_this_redis_password
# ── JWT Secrets (generate with: openssl rand -hex 64) ───────────────────────
JWT_SECRET=change_this_jwt_secret_in_production
JWT_REFRESH_SECRET=change_this_refresh_secret_in_production
# ── Web Push / VAPID (generate with: npx web-push generate-vapid-keys) ──────
VAPID_PUBLIC_KEY=
VAPID_PRIVATE_KEY=
VAPID_EMAIL=admin@yourdomain.com
# ── Email — Resend (https://resend.com) ─────────────────────────────────────
RESEND_API_KEY=re_your_api_key_here
EMAIL_FROM=SecureChat <noreply@yourdomain.com>
# ── App URL ─────────────────────────────────────────────────────────────────
# Must match your domain — used for CORS, push notification links, and emails
CLIENT_URL=https://yourdomain.com
# ── Ports ───────────────────────────────────────────────────────────────────
HTTP_PORT=80
HTTPS_PORT=443
# ── Admin ───────────────────────────────────────────────────────────────────
# Comma-separated usernames with access to the admin panel
# Must be set on both frontend (build arg) and backend (env var)
ADMIN_USERNAMES=yourusername
# ── Node environment ────────────────────────────────────────────────────────
NODE_ENV=productionNever commit your
.envfile. Generate all secrets fresh — never use the placeholder values in production.
| Variable | Required | Purpose |
|---|---|---|
MONGO_PASSWORD |
Yes | Change before first run |
REDIS_PASSWORD |
Yes | Change before first run |
JWT_SECRET |
Yes | Access token signing |
JWT_REFRESH_SECRET |
Yes | Refresh token signing |
CLIENT_URL |
Yes | CORS origin, email links, push notification URLs |
ADMIN_USERNAMES |
Yes | Comma-separated list of admin usernames |
VAPID_PUBLIC_KEY |
Optional | Required for push notifications |
VAPID_PRIVATE_KEY |
Optional | Required for push notifications |
VAPID_EMAIL |
Optional | Required for push notifications |
RESEND_API_KEY |
Optional | Required for all email features |
EMAIL_FROM |
Optional | Required alongside RESEND_API_KEY |
Users listed in ADMIN_USERNAMES see an Admin button in the sidebar. The variable controls both the UI and the backend API.
The admin panel provides:
Stats tab — total users, messages, active users today, storage used, message activity chart
Users tab — search and filter all users; tap any user to:
- View profile and last seen
- Suspend or delete the account
- Reset password (sends an email reset link)
- Reset 2FA (for locked-out users)
- Manually verify email
Settings tab — toggle open/close registration, generate and revoke time-limited invite links with optional email delivery, configure email notification preferences
Audit Log tab — full timestamped log of all admin actions with search and pagination
Push notifications not working
- Requires HTTPS with a valid domain (not self-signed)
- On iOS, the app must be installed as a PWA (Add to Home Screen) and iOS 16.4+
- Check that
VAPID_*keys are set and the backend was restarted after adding them
Password reset emails not arriving
- Verify
RESEND_API_KEYandEMAIL_FROMare set in.env - Check that your Resend account has the sending domain verified
- Restart the backend after any
.envchanges:docker compose up -d backend
Admin panel returns 403
ADMIN_USERNAMESmust be set in.envand passed to both the frontend build and the backend container- After changing it, rebuild:
docker compose up -d --build
Certificate warning on first load
- Expected when using a self-signed cert (no
--domainflag) - Re-run
./setup.sh --domain=yourdomain.comonce your domain is pointed at the server
Messages not decrypting after password change
- Keys are re-wrapped on password change — if you see decryption errors, log out and back in to re-fetch the updated key material
See CHANGELOG.md for full release history.
MIT License — Copyright (c) 2026 Roman Parish
See LICENSE for full details.
Built for privacy.







