Elahe Messenger is an open-source, self-hosted, end-to-end encrypted messaging platform built for teams, communities, and individuals who demand full control over their data. It combines the power of Next.js 15, React 19, and Socket.IO on a Node.js runtime, backed by Prisma ORM with PostgreSQL (or SQLite for local development) and optionally scaled horizontally via Redis.
Client apps encrypt direct-message content before transmission. The server primarily handles ciphertext payloads, while still processing operational metadata (membership, timestamps, audit/security events).
- Features
- Architecture
- Requirements
- Quick Start
- Manual Installation
- Configuration
- Docker Deployment
- Usage Guide
- API & Integrations
- Observability
- Testing
- Security
- Capability Maturity
- Crypto Status
- Runtime Topology
- Threat Model
- Project Structure
- Contributing
- License
| Category | Capabilities |
|---|---|
| 🔐 Encryption | Browser-side E2EE for direct messages (ECDH-P256, HKDF-SHA256, AES-256-GCM); device-bound key bundles; pre-key rotation; safety-number verification; advanced ratcheting remains transitional |
| 💬 Messaging | Real-time DMs, group chats, channels, message reactions, edits, replies/threads, server-side search, drafts, disappearing messages, read receipts |
| 📎 Attachments | Encrypted secure upload/download with MIME/extension allowlist; pluggable object storage (local filesystem or S3/MinIO) |
| 🎙️ Voice & Video | WebRTC 1:1 calls with TURN/STUN support and media manager |
| 👥 Social | Contact management, community groups, invite links, member roles |
| 🛡️ Security | TOTP/2FA (RFC 6238), session binding (IP/User-Agent), CSRF/origin checks, rate limiting (HTTP + WebSocket), local math captcha or reCAPTCHA, audit logs, password policy enforcement |
| 🔑 Authentication | Username/password, OAuth/SSO (Google, GitHub, generic OIDC), password recovery with recovery questions, bootstrap admin workflow |
| 🤖 Bot Platform | Built-in bot framework with webhook registration, programmatic message sending, and per-bot auth tokens |
| 🧭 Admin | User management, ban/verify controls, moderation workflows, settings panel, reports/KPI dashboard, scheduled backups |
| 📊 Observability | OpenTelemetry tracing, structured Pino JSON logs, Prometheus-style metrics endpoint, liveness/readiness probes |
| 📦 DevOps | Docker Compose (base + prod override + split runtime), one-line installer, Caddy auto-SSL, health checks, PgBouncer auto-detection |
| 🗄️ Database | Prisma ORM, PostgreSQL 16 (prod), SQLite (dev), migration deploy fail-fast, bootstrap vs least-privilege runtime roles |
| 📱 PWA | Installable app shell with Workbox service worker, cached static assets, offline draft queue |
| 🔔 Push | VAPID web-push notifications, optional Firebase FCM fallback |
| 🌐 i18n | Localized UI strings with right-to-left support |
| 🧪 Testing | Vitest unit + integration projects, Playwright e2e, CodeQL + Trivy + Gitleaks in CI |
- Authenticate and bind session: user signs in; secure cookie session remains guarded by CSRF/origin checks.
- Load client key material: E2EE keys are generated/loaded in-browser (Web Crypto + IndexedDB).
- Encrypt on client: message content is encrypted before transmission; server should not require plaintext.
- Send in real-time: ciphertext is sent over HTTPS/WSS to
server.tsand Socket.IO. - Apply server-side guards: membership, authorization, rate limits, anti-abuse rules, and audit logging are enforced.
- Persist and distribute: encrypted payload is stored via Prisma in PostgreSQL; optional Redis supports pub/sub scaling.
- Deliver to recipient devices: authorized recipient sessions receive ciphertext in real-time.
- Decrypt only on recipient client: browser decrypts locally and updates delivery/read state.
flowchart TD
A[User login + secure session] --> B[Load E2EE keys in browser]
B --> C[Compose message]
C --> D[Client-side encryption]
D --> E[Send ciphertext over HTTPS/WSS]
E --> F[server.ts + Next.js + Socket.IO]
F --> G{Security checks: membership/rate/authz}
G -->|Allowed| H[(PostgreSQL via Prisma)]
G -->|Allowed| I[(Redis optional: Pub/Sub)]
H --> J[Real-time delivery to recipient]
I --> J
J --> K[Recipient browser decrypts]
K --> L[Update delivered/read state]
| Dependency | Minimum Version | Notes |
|---|---|---|
| Node.js | 20 LTS | Required for native crypto APIs |
| npm | 10+ | Package management |
| PostgreSQL | 15+ | Production database |
| Redis | 6+ | Optional; enables clustering |
| Docker + Compose | v2+ | Recommended for production |
Supported installer platform: Linux (amd64/x86_64, arm64/aarch64).
# 1) One-line install (works for root and non-root users)
curl -fsSL https://raw.githubusercontent.com/ehsanking/ElaheMessenger/main/install.sh | ( [ "$(id -u)" -eq 0 ] && bash || sudo bash )
# 2) Optional: Download from a pinned tag for reproducible installs
TAG="<release-tag>"
curl -fsSLo install.sh "https://raw.githubusercontent.com/ehsanking/ElaheMessenger/${TAG}/install.sh"
# 3) Verify checksum (recommended)
# Replace with the checksum published for the chosen release/tag.
echo "<sha256> install.sh" | sha256sum -c -
# 4) Inspect installer before running
less install.sh
# 5) Run (installer auto-elevates with sudo when possible)
sudo bash install.shThe one-line installer performs the full production lifecycle (preflight checks, environment configuration, and service launch). There is no separate setup command for production installs in this repository.
Reproducible alternatives:
# Pinned tag
sudo INSTALL_REF=<release-tag> bash install.sh
# Pinned commit
sudo INSTALL_REF=<40-char-commit-sha> bash install.shNon-interactive automation (CI/provisioning-safe):
sudo INSTALL_NONINTERACTIVE=true \
INSTALL_MODE=fresh \
INSTALL_USE_DOMAIN=false \
INSTALL_REF=<release-tag> \
bash install.shOptional non-interactive domain mode:
sudo INSTALL_NONINTERACTIVE=true \
INSTALL_MODE=fresh \
INSTALL_USE_DOMAIN=true \
INSTALL_DOMAIN_NAME=chat.example.com \
INSTALL_SSL_EMAIL=admin@example.com \
INSTALL_REF=<release-tag> \
bash install.shUnsafe/dev-only (mutable branch head):
curl -fsSLo install.sh https://raw.githubusercontent.com/ehsanking/ElaheMessenger/main/install.sh
sudo INSTALL_REF=main bash install.sh- Fresh install (new deployment)
- Upgrade (safe in-place update, preserves
.envsecrets/data) - Reinstall (backs up existing directory first, then re-installs)
Installer safety behavior:
- Prompts for a source ref strategy (latest tag recommended, or explicit tag/commit); mutable
mainhead is still available but warned. - Preserves operator-managed config by default on upgrade (
.env,Caddyfile, compose overrides). Regeneration happens only when explicitly selected. - Preserves existing
.npmrcand registry/mirror settings; creates a default.npmrconly when missing. - Upgrade now prompts for proxy behavior: preserve existing proxy config (default) or regenerate proxy config (for ingress/domain/IP changes).
- Preserves existing production secrets on upgrade (
POSTGRES_*,APP_DB_*,DATABASE_URL, auth/encryption/download secrets, admin credentials) unless you explicitly change values. - Enforces database role separation: bootstrap role (
POSTGRES_*) for DB provisioning and least-privilege runtime role (APP_DB_*) for the appDATABASE_URL. - Creates timestamped upgrade backups (
.env,Caddyfile, compose files) before update steps. - Aborts upgrades when git sync fails or the worktree is dirty (no implicit
rm -rffallback). - Uses Caddy on
:80/:443; in IP-only mode the generatedAPP_URLuseshttp://<server-ip>(no internal:3000mismatch). - Preflight checks validate listener availability on
tcp/80,tcp/443, andudp/443before launch (non-interactive mode fails closed on conflicts). - Never prints bootstrap admin password in terminal output; auto-generated credentials are written once to a local secrets file with restrictive permissions.
- Non-interactive installs are first-class: no hidden interactive dependency; install choices are deterministic and env-driven.
- Verifies post-launch health in explicit phases: container health, local reverse-proxy routing, and external DNS/TLS readiness guidance.
- Fails install when local reverse-proxy routing does not work, and only warns for external DNS/TLS propagation uncertainty.
- Source trust defaults to a pinned tag when available; mutable branch-head installs are opt-in and explicitly warned during installer prompts.
- Fresh/reinstall writes bootstrap admin password to a one-time file (
./runtime/admin-bootstrap-password) and passes it viaADMIN_BOOTSTRAP_PASSWORD_FILE. ADMIN_USERNAME/ADMIN_PASSWORDare create-only by default; ifADMIN_BOOTSTRAP_RESET_EXISTING=trueis used, reset is consumed once per credential set (not repeated on every restart).- Does not auto-enable UFW; firewall changes remain operator-driven.
- Installer hangs in piped mode: run with
INSTALL_NONINTERACTIVE=true(and optionallyINSTALL_MODE,INSTALL_USE_DOMAIN,INSTALL_DOMAIN_NAME). - Ports 80/443 are already used: stop conflicting services; non-interactive installs fail fast on conflicts by design.
- Domain install fails local probe: verify
INSTALL_DOMAIN_NAME/domain prompt value is correct and resolves publicly; installer now validates host-routed proxy behavior locally with--resolve. - Docker Compose missing on Debian/Ubuntu: installer attempts distro compose plugin packages (
docker-compose-plugin/docker-compose-v2) and exits with actionable guidance if unavailable. - Need strict reproducibility: pin
INSTALL_REFto a release tag or commit, notmain.
Detailed runbook: docs/installer-verification-checklist.md.
# 1. Clone the repository
git clone https://github.com/ehsanking/ElaheMessenger.git
cd ElaheMessenger
# 2. Choose environment template
cp .env.example .env
# For production, use:
# cp production.env.example .env
# 3. Edit .env and set all required production values.
# Required for production:
# APP_ENV=production
# DATABASE_URL (PostgreSQL)
# APP_DB_USER / APP_DB_PASSWORD
# APP_URL / ALLOWED_ORIGINS
# JWT_SECRET / SESSION_SECRET / ENCRYPTION_KEY / DOWNLOAD_TOKEN_SECRET
# ADMIN_USERNAME and (ADMIN_PASSWORD or ADMIN_BOOTSTRAP_PASSWORD_FILE)
# LOCAL_CAPTCHA_SECRET when CAPTCHA_PROVIDER=local
# 4. Install dependencies (generates Prisma client automatically)
npm install
# 5. Validate environment before first start
npm run validate:env -- --mode=production
# 6. Apply database migrations
npm run db:migrate:prod
# 7. Build for production
npm run build
# 8. Start
npm startFirst run:
npm installis side-effect free for database state (client generation only). Run DB setup explicitly withnpm run db:init:dev(SQLite/dev) ornpm run db:migrate:prod(PostgreSQL/prod).
- Docker startup now logs explicit bootstrap stages (env validation, DB wait, migration deploy, server handoff) in
docker-entrypoint.sh. - Migration failures are fail-fast and include actionable guidance (
DATABASE_URLreachability, migration history, schema compatibility). - Runtime/API failures return structured safe error payloads with:
error(safe message),errorCode(machine-readable classification),requestId(for correlation in server logs),- optional
action(next step for operators/clients).
- For authentication/bootstrap failures, use the emitted
requestIdto correlate with JSON logs fromlib/logger.ts.
All configuration is done through environment variables. Copy .env.example to .env and set the values below.
Environment loading policy:
- Local development: load
.env, then.env.local(if present) - Docker/production: load only injected env values /
.env(ignore.env.local)
| Variable | Default | Description |
|---|---|---|
DATABASE_URL |
SQLite in .env.example |
PostgreSQL connection string for production |
POSTGRES_USER |
(none) | Bootstrap/admin PostgreSQL role (provisioning only) |
POSTGRES_PASSWORD |
(none) | Bootstrap/admin PostgreSQL password |
POSTGRES_DB |
elahe |
PostgreSQL database name |
APP_DB_USER |
(none) | Least-privilege runtime DB user for the app |
APP_DB_PASSWORD |
(none) | Least-privilege runtime DB password |
MIGRATION_DATABASE_URL |
(none) | PostgreSQL URL for migration/provisioning role (recommended: bootstrap role) |
APP_URL |
http://localhost:3000 |
Public base URL of the application |
NODE_ENV |
development |
Set to production for production builds |
PORT |
3000 |
HTTP server port |
| Variable | Description |
|---|---|
JWT_SECRET |
HMAC-SHA256 signing secret for session tokens (≥ 32 chars) |
SESSION_SECRET |
Dedicated session-cookie signing secret (≥ 32 chars, no cross-domain reuse) |
ENCRYPTION_KEY |
AES encryption key for sensitive fields |
DOWNLOAD_TOKEN_SECRET |
Attachment/token signing secret (independent from session secret) |
LOCAL_CAPTCHA_SECRET |
HMAC key for local captcha challenge signing in production |
CAPTCHA_PROVIDER |
recaptcha (default) or local |
ADMIN_USERNAME |
Initial admin username (required; no default) |
ADMIN_PASSWORD |
Optional inline bootstrap password (legacy-compatible) |
ADMIN_BOOTSTRAP_PASSWORD_FILE |
Optional bootstrap password file path (preferred in production) |
ADMIN_BOOTSTRAP_STRICT |
Fail startup when bootstrap cannot complete (true on fresh installs) |
| Variable | Description |
|---|---|
VAPID_PUBLIC_KEY |
Web Push VAPID public key |
VAPID_PRIVATE_KEY |
Web Push VAPID private key |
VAPID_EMAIL |
Contact email for VAPID |
| Variable | Description |
|---|---|
REDIS_URL |
e.g. redis://localhost:6379 — enables Socket.IO clustering |
| Variable | Default | Description |
|---|---|---|
RATE_LIMIT_WINDOW_MS |
900000 |
Rate limit window in milliseconds (15 min) |
RATE_LIMIT_MAX_REQUESTS |
100 |
Max requests per window per IP |
SOCKET_RATE_LIMIT_WINDOW_MS |
10000 |
Socket rate limit window (10 s) |
SOCKET_RATE_LIMIT_MAX |
30 |
Max socket events per window |
- Dependabot is configured for npm, Docker, and GitHub Actions (
.github/dependabot.yml). - Container image scanning runs with Trivy and uploads SARIF to GitHub Security tab (
.github/workflows/container-security.yml). - Repository secret scanning runs with Gitleaks on push/PR (
.github/workflows/secret-scan.yml).
docker compose up -d# 1) Copy production env template and set strong values
cp production.env.example .env.production
# 2) Start using base + production override compose files
docker compose -f docker-compose.yml -f compose.prod.yaml --env-file .env.production up -d --buildcompose.prod.yaml is an override file for docker-compose.yml (not a standalone compose file).
--env-file .env.production is required so Compose can interpolate ${VAR} values in the compose model (while env_file: only affects container runtime environment).
Security note: define production credentials explicitly via
.env.production(or Docker secrets) before startup.
Use split mode only when you need independent scaling for API/socket and background workers.
# API + worker split using explicit topology override
docker compose \
-f docker-compose.yml \
-f compose.prod.yaml \
-f compose.split.yaml \
--env-file .env.production \
up -d --buildIn split mode, api handles HTTP/Socket.IO only and worker handles background jobs only.
Caddy targets api in this topology, and the base app service is disabled to avoid duplicate schedulers.
Container names and services:
| Service | Container | Description |
|---|---|---|
| App | elahe-app |
Next.js + Socket.IO server |
| Database | elahe-db |
PostgreSQL 16 |
| Reverse proxy | elahe-caddy |
Caddy with automatic Let's Encrypt SSL |
| Port | Exposure | Why |
|---|---|---|
80/tcp |
Public | HTTP challenge + redirect / non-TLS IP mode |
443/tcp |
Public | HTTPS ingress |
443/udp |
Optional Public | HTTP/3 (QUIC) |
5432/tcp |
Private only | PostgreSQL (Docker-internal by default) |
3000/tcp |
Private only | App container behind Caddy |
6379/tcp |
Private only | Redis (if used) |
The provided compose files keep PostgreSQL internal-only by default (no
ports:publish fordb). Do not expose5432unless you intentionally need remote database access.
POSTGRES_USER/POSTGRES_PASSWORD: bootstrap/admin database role used for first-time PostgreSQL provisioning.APP_DB_USER/APP_DB_PASSWORD: runtime least-privilege role used by Prisma/app inDATABASE_URL.MIGRATION_DATABASE_URL: role used for schema migrations (prisma migrate deploy); should remain bootstrap/provisioning-scoped.DATABASE_URLshould point toAPP_DB_USER, not the bootstrap account.- Runtime role grants are intentionally limited to application DML/sequence/function access; schema-changing privileges stay in the migration/bootstrap role.
- Treat both bootstrap and runtime DB secrets as sensitive; rotate and store with least access (prefer secret manager or Docker secrets over plaintext files where possible).
SESSION_SECRETis a dedicated session-signing secret and must not be reused as a fallback for unrelated security domains.
- Elahe Messenger auto-detects PgBouncer and appends
pgbouncer=trueto Prisma's runtime URL when:PGBOUNCER_ENABLED=true, orDATABASE_URLhost containspgbouncer, orDATABASE_URLuses port6432.
- Keep
PRISMA_CONNECTION_LIMITconservative when pooling is active to avoid queue buildup in transaction mode. - Practical starting points:
- app instances: 2 → set
PRISMA_CONNECTION_LIMIT=10each - app instances: 4 → set
PRISMA_CONNECTION_LIMIT=5-8each - reserve at least 20% of PostgreSQL
max_connectionsfor migrations, maintenance, and admin sessions.
- app instances: 2 → set
- Database dumps and volume backups can contain sensitive metadata and ciphertext payloads; protect backups with encryption-at-rest and strict access controls.
- If host disk/volume data (
pgdata) is unencrypted and host is compromised, DB contents can be copied even without network DB exposure. - Keep backup artifacts out of git and out of web-served paths.
- Automated backups are scheduled by the worker runtime (
scheduled_backupjob). Do not run an additional backup container in parallel.
The installer intentionally does not enable UFW automatically.
Recommended sequence on Ubuntu/Debian hosts:
# 1) Allow SSH FIRST (use your actual SSH port if not 22)
sudo ufw allow 22/tcp
# 2) Allow web ingress
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# 3) Optional HTTP/3/QUIC
sudo ufw allow 443/udp
# 4) Enable firewall
sudo ufw enable
# 5) Verify
sudo ufw status verbose
sudo ufw status numberedDo not open these publicly unless intentionally required:
5432/tcp(PostgreSQL)3000/tcp(app internal port)6379/tcp(Redis)
Operational safety:
- Docker and host firewalls can interact in non-obvious ways (NAT/forward chains). Validate effective exposure with external scans after changes.
- If locked out, regain console/KVM access and rollback rules with
sudo ufw disable(or delete problematic numbered rules). - Inspect logs via
sudo journalctl -u ufw --since "1 hour ago"andsudo dmesg | rg -i ufw.
Health endpoints:
- Liveness:
GET /api/health/live - Readiness:
GET /api/health/ready(legacyGET /api/healthremains as readiness)
- Web visitors opening
/get the public marketing shell. - Auth flows are isolated under
/auth/*. - The installed PWA starts at
/chat?source=pwa(manifeststart_url) so users land directly in the app shell. /chatis server-guarded: authenticated users see chat; unauthenticated users are redirected to/auth/login?next=/chat.- Login and 2FA completion honor the
nextparameter and return users directly to chat. - Registration redirects smoothly into login with
next=/chatto avoid landing-page bounce loops. - Installed PWA sessions that are not authenticated are routed server-side to
/auth/login?next=/chat(not the public landing page).
- Complete
Quick StartorManual Installation(above) and wait for the container health checks to reporthealthy. - Open the public URL (
APP_URL) in a browser — you will land on the marketing page. - Navigate to
/auth/loginand sign in with the bootstrap admin credentials:- Username: value of
ADMIN_USERNAME - Password: value of
ADMIN_PASSWORD, or the one-time password stored at./runtime/admin-bootstrap-password(produced by fresh/reinstall flows)
- Username: value of
- On first login,
ADMIN_BOOTSTRAP_FORCE_PASSWORD_CHANGE=truewill require you to set a new password. - Enable TOTP/2FA from Settings → Security Center and scan the QR code with any RFC 6238 authenticator (Aegis, Authy, Google Authenticator, etc.).
- Open Admin → Users to invite users, send invite links, or enable self-registration.
- To allow open registration, toggle it in Admin → Settings → Registration.
- Invite links are single-use and expire after the configured TTL.
- Open
/chatafter signing in. The browser will generate E2EE key material and store it in IndexedDB on first load. - Pick a contact from the sidebar and start a 1:1 conversation — each message is encrypted client-side before it leaves the browser.
- To create a group, click New Group, choose members, and publish. Group E2EE is still transitional (see Capability Maturity).
- Attachments are encrypted through the secure upload flow (
/api/upload-secure) and served via tokenized download URLs.
- Moderation: ban, verify, or demote users from Admin → Users.
- Audit log: every admin action is recorded with IP, timestamp, and actor.
- Backups: the worker runtime schedules a
scheduled_backupjob. Manual backups vianpm run backupproduce a timestamped archive underBACKUP_OUTPUT_DIR. - Reports / KPIs: dashboards available at
/admin/reports.
# Re-run the installer in upgrade mode (preserves .env, Caddyfile, compose overrides)
sudo INSTALL_MODE=upgrade INSTALL_REF=<new-release-tag> bash install.shOr, for a manual upgrade:
git fetch --tags origin
git checkout <new-release-tag>
npm ci --no-fund --ignore-scripts
npx prisma generate
npm run db:migrate:prod
npm run build
# restart the service (systemd / docker compose / PM2)Elahe Messenger ships with a first-class REST API. The full machine-readable spec is served at:
- OpenAPI JSON:
GET /api/docs/openapi.json - Interactive docs:
GET /api/docs
Key routes:
| Method | Path | Purpose |
|---|---|---|
POST |
/api/login |
Start a username/password login flow |
POST |
/api/2fa |
Verify the TOTP challenge |
GET |
/api/session |
Introspect the current session |
POST |
/api/password-recovery |
Initiate recovery with recovery questions |
GET |
/api/messages/search |
Full-text search over accessible conversations |
GET |
/api/messages/sync |
Delta sync for the chat UI |
GET |
/api/messages/thread/[messageId] |
Fetch replies in a thread |
POST |
/api/drafts |
Persist encrypted drafts |
POST |
/api/upload-secure |
Encrypted attachment upload |
GET |
/api/upload-secure/[fileId] |
Tokenized download |
POST |
/api/push/subscribe |
Register a VAPID push subscription |
GET |
/api/health/live |
Liveness probe |
GET |
/api/health/ready |
Readiness probe |
GET |
/api/metrics |
Prometheus-style metrics (requires internal scrape auth) |
| Method | Path | Purpose |
|---|---|---|
POST |
/api/e2ee/register |
Register identity keys |
POST |
/api/e2ee/register-bundle |
Upload a pre-key bundle |
GET |
/api/e2ee/prekey-bundle/[userId] |
Fetch a peer's bundle |
GET |
/api/e2ee/public-keys/[userId] |
Fetch peer public keys |
POST |
/api/e2ee/devices/register |
Register a new device |
GET |
/api/e2ee/devices |
List registered devices |
POST |
/api/e2ee/sessions/bootstrap |
Bootstrap an E2EE session |
GET |
/api/e2ee/group-keys |
Fetch group keys |
POST |
/api/e2ee/group-keys/rotate |
Rotate group keys |
GET |
/api/e2ee/migration-readiness |
Check readiness for E2EE migration |
GET |
/api/e2ee/runtime-status |
Report runtime E2EE status |
Elahe Messenger supports optional third-party sign-in through @auth/core. Configure any subset of the following providers via environment variables:
# Google
OAUTH_GOOGLE_CLIENT_ID=
OAUTH_GOOGLE_CLIENT_SECRET=
# GitHub
OAUTH_GITHUB_CLIENT_ID=
OAUTH_GITHUB_CLIENT_SECRET=
# Generic OIDC
OAUTH_OIDC_ISSUER=
OAUTH_OIDC_CLIENT_ID=
OAUTH_OIDC_CLIENT_SECRET=The OAuth finalization endpoint is POST /api/auth/oauth/finalize. Only configured providers are exposed on the login page.
Elahe Messenger ships with a built-in bot framework:
# Register a bot (admin only)
curl -X POST https://chat.example.com/api/bots/register \
-H "Authorization: Bearer <admin-token>" \
-H "Content-Type: application/json" \
-d '{"name":"my-bot","webhookUrl":"https://bot.example.com/hook"}'
# Send a message from a bot
curl -X POST https://chat.example.com/api/bots/<botId>/send \
-H "Authorization: Bearer <bot-token>" \
-H "Content-Type: application/json" \
-d '{"conversationId":"...","content":"Hello from my bot"}'Incoming events are delivered to the bot's webhook (POST /api/bots/[botId]/webhook).
Voice and video use WebRTC with optional TURN/STUN relays. Configure:
TURN_URL=turn:turn.example.com:3478
TURN_USERNAME=<turn-user>
TURN_CREDENTIAL=<turn-credential>
NEXT_PUBLIC_TURN_URL=turn:turn.example.com:3478
NEXT_PUBLIC_TURN_USERNAME=<public-turn-user>
NEXT_PUBLIC_TURN_CREDENTIAL=<public-turn-credential>Only the NEXT_PUBLIC_* variants are exposed to the browser bundle.
By default, encrypted attachments are stored on the local filesystem at OBJECT_STORAGE_ROOT. Swap to an S3-compatible store (AWS S3, MinIO, Wasabi, Cloudflare R2) by implementing/enabling the S3 driver and providing AWS credentials through @aws-sdk/client-s3 environment variables.
Elahe Messenger ships instrumentation out of the box.
- JSON logs emitted to stdout (captured by Docker / systemd).
- Log level controlled by
LOG_LEVEL(defaultinfo). - Every error response carries a
requestId— grep for it to correlate across logs.
Enable OTLP export with any of these:
OTEL_EXPORTER_OTLP_ENDPOINT=https://otel-collector.example.com
OTEL_SERVICE_NAME=elahe-messenger
OTEL_RESOURCE_ATTRIBUTES=deployment.environment=productionTracing is wired via @opentelemetry/sdk-node and instruments HTTP, Prisma, and outbound fetches.
- Prometheus-style metrics endpoint:
GET /api/metrics - Health endpoints:
- Liveness:
GET /api/health/live - Readiness:
GET /api/health/ready(also exposed as the legacyGET /api/health)
- Liveness:
- When running via Docker, configure the host's log driver (
json-filewithmax-size+max-file, or switch tojournald/fluentd). - When running bare-metal under systemd, rely on
journalctl --vacuum-time=.
Elahe Messenger uses Vitest for unit/integration tests and Playwright for end-to-end tests.
# Run the full Vitest suite (unit + integration)
npm test
# Run only the "unit" project
npm run test -- --project unit
# Run only the "integration" project
npm run test -- --project integration
# Watch mode for development
npm run test:watch
# Playwright end-to-end (requires Playwright browsers installed)
npx playwright install --with-deps
npx playwright testTest projects are declared in vitest.config.ts:
- unit — default test files under
tests/**/*.test.ts(excludingtests/e2e/**). - integration — installation / deployment smoke tests that validate installer guardrails and compose topology.
The CI pipeline runs both Vitest projects plus a full npm run build, Docker image build, shellcheck on install.sh and docker-entrypoint.sh, and deployment smoke tests.
- CodeQL static analysis (
.github/workflows/codeql.yml). - Trivy container image scanning with SARIF upload (
.github/workflows/container-security.yml). - Gitleaks secret scanning on push/PR (
.github/workflows/secret-scan.yml). - Hadolint Dockerfile linting (
.github/workflows/hadolint.yml). - Dependabot for npm, Docker, and GitHub Actions (
.github/dependabot.yml).
Elahe Messenger is designed with a privacy-first model and explicit trust boundaries:
- Implemented now: Direct-message E2EE uses browser-side
ECDH-P256+HKDF-SHA256+AES-256-GCM. - Not yet shipped: Do not assume full X3DH/Double Ratchet parity for all paths; group/channel E2EE and advanced ratcheting remain transitional.
- Server visibility: Operators can access service metadata (accounts, membership, delivery/audit timestamps, network/session security signals) even when message bodies are ciphertext.
- Session Security: Session tokens are HMAC-signed, HttpOnly, SameSite=Strict cookies with optional IP and User-Agent binding; cookie
Secureis derived fromAPP_URLscheme (or explicitCOOKIE_SECUREoverride). - 2FA/TOTP: Password step now creates a short-lived pending-login challenge; TOTP verification requires that one-time challenge.
- Rate Limiting: Per-IP limits enforced at both the HTTP and WebSocket layers, backed by Redis when available.
- Draft Privacy: Server-side draft persistence stores encrypted draft fields only; plaintext
clientDraftcontent is not persisted. - File Upload Policy: Secure uploads enforce server allowlist checks by extension + MIME (case-insensitive) and reject MIME mismatches.
- Audit Logging: Admin actions are recorded with IP, timestamp, and actor for forensic traceability.
See detailed docs:
For vulnerability disclosures, see SECURITY.md.
| Capability | Status | Notes |
|---|---|---|
| Direct messaging | Stable | Core 1:1 messaging path is operational and covered by existing authz checks. |
| Group messaging | Beta | Functional, but not E2EE-complete yet. |
| Encrypted attachments | Beta | Secure attachment route exists; keep using protected upload/download flows only. |
| Admin tooling | Stable | Includes moderation and audit workflows. |
| Push notifications | Beta | Production-capable with environment/provider dependencies. |
| Multi-device support | Experimental | Device bundle/session model is present but still evolving. |
| Ratcheting / advanced E2EE | Experimental | Transitional runtime support exists; do not market as fully completed protocol guarantees. |
| Crypto verification UX | Planned | Operator/user-facing verification flows need stronger UX and guidance. |
| Offline reliability | Beta | PWA shell and draft/offline queue support exist with network-dependent sync behavior. |
See docs/crypto-status.md for implementation-accurate cryptographic guarantees and known gaps.
See docs/runtime-topology.md for runtime separation, startup flow, and future split points.
See docs/threat-model.md for trust assumptions, metadata visibility, and hardening opportunities.
elahe-messenger/
├── app/ # Next.js App Router pages and API routes
│ ├── actions/ # Server Actions (auth, messages, admin)
│ ├── api/ # REST API route handlers (auth, e2ee, bots, health, metrics, ...)
│ ├── auth/ # Login, register, 2FA, recovery pages
│ ├── chat/ # Chat UI, profile, security center
│ └── admin/ # Admin panel pages
├── components/ # Shared React components
├── lib/ # Core server-side modules
│ ├── session.ts # Session management
│ ├── crypto.ts # E2EE primitives
│ ├── prisma.ts # Database client singleton
│ ├── rate-limit.ts # Rate limiting logic
│ ├── local-captcha.ts # Stateless math captcha
│ ├── logger.ts # Pino structured logger
│ └── telemetry.ts # OpenTelemetry setup
├── prisma/ # Prisma schema and migrations
├── public/ # Static assets (logo, manifest, service worker)
├── scripts/ # Utility scripts (db-setup, backup, validate-env)
├── tests/ # Vitest unit + integration tests
│ └── e2e/ # Playwright end-to-end specs
├── docs/ # Design docs (crypto status, threat model, topology)
├── .github/ # GitHub Actions workflows and Dependabot config
├── server.ts # Custom Node.js server (Socket.IO)
├── instrumentation.ts # Next.js instrumentation hook (OpenTelemetry)
├── next.config.ts # Next.js config (Workbox, standalone output)
├── docker-compose.yml # Development Compose
├── compose.prod.yaml # Production override for docker-compose.yml
├── compose.split.yaml # Optional split-runtime topology override
├── Dockerfile # Multi-stage production image
├── docker-entrypoint.sh # Runtime bootstrap (env validation, migrations, server)
└── install.sh # One-line production installer
Contributions are welcome. Please follow these steps:
- Fork the repository and create a feature branch:
git checkout -b feat/my-feature - Follow the existing code style — run
npm run formatandnpm run lintbefore committing - Write or update tests where applicable:
npm test - Commit using Conventional Commits:
feat:,fix:,docs:, etc. - Open a Pull Request against
mainwith a clear description of changes
npm run dev # Start dev server with hot-reload
npm run build # Production build
npm run lint # ESLint check (warnings fail the command)
npm run format # Prettier auto-format
npm test # Run Vitest test suite
npm run db:init:dev # SQLite/dev bootstrap
npm run db:migrate:prod # PostgreSQL/prod migrations (fail-fast)
npm run backup # Create database backup archiveReleased under the MIT License.
Copyright © 2026 Elahe Messenger Contributors.
Built with ❤️ by @ehsanking and contributors.
t.me/kingithub
If this project helps you, you can support its maintenance:
- USDT (TRC20 / Tether):
TKPswLQqd2e73UTGJ5prxVXBVo7MTsWedU - TRON (TRX):
TKPswLQqd2e73UTGJ5prxVXBVo7MTsWedU
