diff --git a/README_DE.md b/README_DE.md
index fd8eb35..3f3a2ee 100644
--- a/README_DE.md
+++ b/README_DE.md
@@ -10,367 +10,173 @@ Eine Instant-Messaging-App im WeChat-Stil mit Ende-zu-Ende-Verschlüsselung übe
---
-## Features
-| Feature | Description |
-|---------|-------------|
-| 🔐 End-to-End Encryption | Stateless ECDH + XSalsa20-Poly1305 — ephemeral keys per message, forward secrecy |
-| 🗑️ Zero-Knowledge Server | Server stores only ciphertext; private keys never leave the device |
-| 📹 Video & Voice Calls | WebRTC P2P (1:1) + Mesh (group), Cloudflare TURN for NAT traversal |
-| 👥 Group Chat | Up to 2000 members, plain-text messages (no encryption), Do Not Disturb mode, member management |
-| ⏱️ Auto-Delete Messages | 5 tiers (never / 1 day / 3 days / 1 week / 1 month), settable by either party in DMs, owner-only in groups |
-| 🔔 Push Notifications | Web Push (VAPID) + OneSignal dual-channel — reach users even when offline |
-| 🌐 Multi-Language | Chinese, English, Japanese, Korean, French, German, Russian, Spanish — auto-detect + manual switch |
-| 📱 iOS — No Enterprise Cert | PWA via Safari "Add to Home Screen", works permanently without Apple signing |
-| 💬 Rich Messaging | Text, images, voice messages, 64-emoji panel, delivery receipts, typing indicators |
-| 🌐 Moments | WeChat-style social feed: text + up to 9 photos, likes (friend avatars), comments, tag-based visibility control |
-| 🏷️ Friend Tags | Assign multiple tags to friends (12-color preset palette), filter contacts by tag |
-| 🗂️ R2 Object Storage | Cloudflare R2 for image/voice files — optional public CDN URL |
-| 🏗️ Self-Hostable | Docker Compose one-command deployment; Node.js + Redis multi-node ready |
+## Funktionen
+
+| Funktion | Beschreibung |
+|----------|-------------|
+| 🔐 E2E-Verschlüsselung | Zustandsloses ECDH + XSalsa20-Poly1305 — ephemere Schlüssel pro Nachricht, Forward Secrecy |
+| 🗝️ Zero-Knowledge-Server | Server speichert nur Chiffretext; private Schlüssel verlassen niemals das Gerät |
+| 📹 Video-/Sprachanrufe | WebRTC P2P (1:1) + Mesh (Gruppe), Cloudflare TURN für NAT-Traversal |
+| 👥 Gruppenchat | Bis zu 2000 Mitglieder, Klartextnachrichten, Nicht-stören-Modus, Mitgliederverwaltung |
+| ⏱️ Auto-Löschung | 5 Stufen (nie/1T/3T/1W/1M), in DMs beidseitig einstellbar, in Gruppen nur Inhaber |
+| 🔔 Push-Benachrichtigungen | Web Push (VAPID) + OneSignal Dual-Kanal |
+| 🌐 Mehrsprachig | ZH/EN/JA/KO/FR/DE/RU/ES — automatische Erkennung + manuelle Umschaltung |
+| 📱 iOS ohne Unternehmenszertifikat | PWA über Safari „Zum Home-Bildschirm", funktioniert dauerhaft ohne Apple-Signatur |
+| 💬 Rich Messaging | Text, Bilder, Sprachnachrichten, 64-Emoji-Panel, Lesebestätigungen |
+| 🌐 Momente | Sozialer Feed: Text + bis zu 9 Fotos, Likes (Freunde-Avatare), Kommentare, Tag-basierte Sichtbarkeit |
+| 🏷️ Freunde-Tags | Mehrere Tags pro Freund (12-Farben-Palette), Kontakte nach Tags filtern |
+| 🗂️ R2-Speicher | Cloudflare R2 für Bild-/Audiodateien — optionale CDN-URL |
+| 🏗️ Self-Hosting | Docker Compose Ein-Befehl-Deployment |
---
-## Tech Stack
+## Technologie-Stack
```
Backend (server/)
Node.js 20 + Express + ws
- MySQL 8.0 — users, messages (persisted ciphertext)
- Redis — online presence + cross-node routing
- Cloudflare R2 — image/voice file storage (S3-compatible API)
- JWT + bcrypt authentication
+ MySQL 8.0 — Benutzer, Nachrichten (verschlüsselt)
+ Redis — Online-Status + Knotenübergreifendes Routing
+ Cloudflare R2 — Bild-/Audio-Dateispeicher (S3-kompatible API)
+ JWT + bcrypt Authentifizierung
Frontend (client/)
- Native HTML + Vanilla JS (ESM, no bundler required)
+ Natives HTML + Vanilla JS (ESM, kein Bundler)
libsodium-wrappers (WebAssembly — Curve25519 / XSalsa20-Poly1305)
- WebRTC API — video / voice calls
+ WebRTC API — Video-/Sprachanrufe
PWA: manifest.json + Service Worker
-Cryptographic Layer
- Stateless ECDH + XSalsa20-Poly1305 — ephemeral keypair per message
- Four-tier key persistence: memory → localStorage → sessionStorage → IndexedDB
- All private keys stored on-device only — never sent to the server
+Kryptographieschicht
+ Zustandsloses ECDH + XSalsa20-Poly1305 — ephemeres Schlüsselpaar pro Nachricht
+ 4-Stufen-Schlüsselpersistenz: Speicher → localStorage → sessionStorage → IndexedDB
+ Alle privaten Schlüssel nur auf dem Gerät — nie an den Server gesendet
```
---
-## Quick Start
+## Schnellstart
-### Option 0: Zeabur One-Click Cloud Deploy
+### Option 0: Zeabur Ein-Klick-Cloud-Deployment
[](https://zeabur.com/templates/P2J7Y3?referralCode=619dev)
> [!NOTE]
-> One manual step is required after the template deploys, otherwise login/register won't work:
-> 1. Go to Zeabur Console → **server service** → Environment Variables → copy the value of `ZEABUR_WEB_URL` (e.g. `http://10.43.x.x:3000`)
-> 2. Go to **client service** → Environment Variables → add variable `SERVER_URL` = the value copied above
-> 3. Restart the client service
+> Nach dem Deployment ist ein manueller Schritt erforderlich:
+> 1. Zeabur-Konsole → **server**-Dienst → Umgebungsvariablen → `ZEABUR_WEB_URL` kopieren
+> 2. **client**-Dienst → Umgebungsvariablen → `SERVER_URL` = kopierter Wert
+> 3. Client-Dienst neu starten
-**Known notes:**
-- On first startup, the server automatically creates all database tables (`CREATE TABLE IF NOT EXISTS`) — no manual SQL import needed
-- Redis runs without a password inside the cluster (intra-cluster network isolation is sufficient)
-- If MySQL access is denied, manually set `DB_PASS` on the server service to the value of `MYSQL_ROOT_PASSWORD` from the MySQL service
-- To get the **internal IP** of any service container, open that service's Terminal in the Zeabur console and run:
- ```bash
- hostname -i
- ```
-
----
-
-### Option 1: Docker Compose (Recommended — no local build needed)
+### Option 1: Docker Compose (empfohlen)
```bash
-# Clone the repository
git clone && cd paperphone
-
-# Copy and edit environment variables
cp server/.env.example server/.env
-# Fill in: DB_PASS / JWT_SECRET / CF_CALLS_APP_ID etc.
-
-# Pull images and start everything
+# Variablen bearbeiten: DB_PASS / JWT_SECRET / R2_* usw.
docker compose up -d
-
-# Check service status
-docker compose ps
-
-# Open in browser
open http://localhost
```
-> Pre-built images on Docker Hub:
-> - `facilisvelox/paperphone-client:latest`
-> - `facilisvelox/paperphone-server:latest`
->
-> **Note**: The server automatically initialises the database schema on first startup — no manual SQL import required.
+> Docker-Hub-Images: `facilisvelox/paperphone-client:latest` und `facilisvelox/paperphone-server:latest`
-### Option 2: Manual Local Start
-
-#### 1. Prepare the environment
+### Option 2: Lokaler manueller Start
```bash
-# Copy and edit environment variables
-cp server/.env.example server/.env
-# Fill in DB_HOST / DB_PASS / REDIS_HOST / R2_* etc.
+# Backend
+cd server && npm install && npm run dev # → http://localhost:3000
-# Note: the server auto-runs schema.sql on first startup
-```
-
-#### 2. Start the backend
-
-```bash
-cd server
-npm install
-npm run dev # → http://localhost:3000
-```
-
-#### 3. Start the frontend
-
-```bash
-npx serve client -p 8080
-# → http://localhost:8080
+# Frontend
+npx serve client -p 8080 # → http://localhost:8080
```
---
-## Video Call Configuration
-
-Video and voice calls use WebRTC P2P and work out of the box on the same LAN. For cross-network calls, a TURN server is required for NAT traversal.
-
-### Using Cloudflare TURN (Recommended)
-
-1. Log in to [Cloudflare Dashboard](https://dash.cloudflare.com) → **Workers & Pages** → **Calls** → create an App
-2. Copy the **App ID** and **App Secret** (Token Key)
-3. Add them to `server/.env`:
+## Videoanruf-Konfiguration — Cloudflare TURN
```env
CF_CALLS_APP_ID=your_app_id_here
CF_CALLS_APP_SECRET=your_app_secret_here
```
-4. Restart the backend — TURN credentials are fetched automatically per call session (TTL: 86 400 s)
+| Typ | Transport | Empfohlen für |
+|-----|-----------|---------------|
+| 1:1 Video | WebRTC P2P + TURN | Alle Szenarien |
+| 1:1 Sprache | WebRTC P2P + TURN | Alle Szenarien |
+| Gruppenanruf | WebRTC Mesh | Bis zu 6 Teilnehmer |
-> **Without credentials**: the server falls back to STUN only (Google + Cloudflare public STUN). Calls work on LAN without any extra configuration.
-
-### Call Types
-
-| Type | Transport | Recommended for |
-|------|-----------|-----------------|
-| 1:1 Video Call | WebRTC P2P + TURN | All scenarios |
-| 1:1 Voice Call | WebRTC P2P + TURN | All scenarios |
-| Group Video / Voice | WebRTC Mesh (full-mesh) | Up to 6 participants |
+> **Ohne Konfiguration**: Nur STUN. LAN-Anrufe funktionieren ohne Einrichtung.
---
-## Push Notification Configuration
+## Push-Benachrichtigungen
-Offline message notifications are delivered through **two channels** for maximum delivery rate:
-
-| Channel | Platforms | Configuration |
-|---------|-----------|---------------|
-| Web Push (VAPID) | Browsers (Chrome/Edge/Firefox) + iOS PWA (Safari 16.4+) | VAPID keys |
-| OneSignal | Native Android/iOS apps via Median.co | OneSignal App ID + REST Key |
-
-### Configuring Web Push
-
-1. Generate VAPID keys (one-time):
+| Kanal | Plattformen | Konfiguration |
+|-------|-------------|---------------|
+| Web Push | Browser + iOS PWA (Safari 16.4+) | VAPID-Schlüssel |
+| OneSignal | Native Apps über Median.co | App ID + REST Key |
```bash
-cd server
npx web-push generate-vapid-keys
```
-2. Add to `server/.env`:
+---
-```env
-VAPID_PUBLIC_KEY=your_public_key_here
-VAPID_PRIVATE_KEY=your_private_key_here
-VAPID_SUBJECT=mailto:admin@your-domain.com
-```
+## iOS — Permanente Installation ohne Zertifikat
-3. Restart the server — users can enable notifications from the Settings page
-
-> **iOS users** must first "Add to Home Screen" (PWA), and only iOS 16.4+ is supported.
-
-### Configuring OneSignal (Median.co Native Apps)
-
-1. Create an app on [OneSignal Dashboard](https://onesignal.com) and configure Firebase
-2. Enable OneSignal in Median.co and enter the App ID
-3. Add the OneSignal **App ID** and **REST API Key** to `server/.env`:
-
-```env
-ONESIGNAL_APP_ID=your_onesignal_app_id
-ONESIGNAL_REST_KEY=your_onesignal_rest_api_key
-```
-
-> **When not configured**: push notifications are silently disabled — all other features work normally.
+1. Auf HTTPS-Server bereitstellen → 2. In Safari öffnen → 3. Teilen ⬆️ → 4. „Zum Home-Bildschirm"
---
-## iOS — Permanent No-Cert Deployment
+## Sicherheitsmodell
-1. Deploy to a server with an HTTPS domain (required for WebRTC and Web Crypto APIs)
-2. Open `https://your.domain.com` in **Safari**
-3. Tap the Share button ⬆️ at the bottom of the screen
-4. Select **Add to Home Screen** → **Add**
-
-The app behaves identically to a native app — no Apple enterprise certificate required, no expiry.
-
----
-
-## Production Deployment (Nginx)
-
-```nginx
-server {
- listen 443 ssl http2;
- server_name your.domain.com;
-
- ssl_certificate /path/to/cert.pem;
- ssl_certificate_key /path/to/key.pem;
-
- # Frontend static files
- location / {
- root /path/to/paperphone/client;
- try_files $uri /index.html;
- }
-
- # REST API
- location /api/ {
- proxy_pass http://localhost:3000;
- proxy_set_header Host $host;
- }
-
- # WebSocket (messaging + call signalling)
- location /ws {
- proxy_pass http://localhost:3000;
- proxy_http_version 1.1;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection "upgrade";
- proxy_read_timeout 3600s;
- }
-}
+```
+Registrierung: IK + SPK + 10×OPK lokal generiert, öffentliche Schlüssel hochgeladen
+Nachricht: Ephemeres ECDH → X25519 → XSalsa20-Poly1305
+Server sieht: ✅ Chiffretext + Routing-Metadaten ❌ Klartext / private Schlüssel
```
---
-## Project Structure
+## Datenbankschema
-```
-paperphone/
-├── docker-compose.yml
-├── server/
-│ ├── .env # Environment variables (incl. Cloudflare TURN keys)
-│ └── src/
-│ ├── app.js # Express application
-│ ├── routes/
-│ │ ├── auth.js # Register / Login (incl. X3DH public key upload)
-│ │ ├── users.js # User search / Prekey bundle download
-│ │ ├── friends.js # Friend requests / Accept (incl. offline push)
-│ │ ├── groups.js # Group management
-│ │ ├── messages.js # Historical messages (paginated ciphertext)
-│ │ ├── upload.js # Cloudflare R2 file upload
-│ │ ├── files.js # File proxy (when R2_PUBLIC_URL is not set)
-│ │ ├── moments.js # Moments feed (posts / likes / comments)
-│ │ ├── calls.js # TURN credential issuance
-│ │ └── push.js # Push subscription mgmt (Web Push + OneSignal)
-│ ├── services/
-│ │ ├── push.js # Web Push VAPID service
-│ │ └── onesignal.js # OneSignal REST API service
-│ └── ws/
-│ └── wsServer.js # WebSocket router (messages + call signalling + offline push)
-│
-└── client/
- ├── index.html # SPA entry + PWA meta + Median push bridge
- ├── manifest.json # PWA manifest
- ├── sw.js # Service Worker (offline cache + push notifications)
- └── src/
- ├── style.css # Premium design system (dark/light, glassmorphism)
- ├── app.js # Router + global state + incoming call listener
- ├── api.js # HTTP client
- ├── socket.js # WebSocket client (auto-reconnect)
- ├── i18n.js # Multi-language engine (zh / en / ja / ko / fr / de / ru / es)
- ├── services/
- │ ├── webrtc.js # WebRTC manager — CallManager class
- │ └── pushNotification.js # Push subscription mgmt (Web Push + Median bridge)
- ├── crypto/
- │ ├── ratchet.js # X3DH + Double Ratchet + ML-KEM-768
- │ └── keystore.js # IndexedDB private key store
- ├── pages/
- │ ├── login.js # Login / Register (key generation, language picker)
- │ ├── chats.js # Chat list
- │ ├── chat.js # Chat window (E2E encryption, call buttons)
- │ ├── groups.js # Group list (create group, search)
- │ ├── groupInfo.js # Group info (member management, DND, leave/disband)
- │ ├── contacts.js # Contacts (friend requests, online status)
- │ ├── discover.js # Discover page
- │ ├── profile.js # Me / Settings (language, fingerprint, notifications, PWA)
- │ └── call.js # Call UI (incoming / active / multi-party video)
-```
+11 Tabellen, automatisch beim ersten Start erstellt:
+
+| Tabelle | Zweck |
+|---------|-------|
+| `users` | Benutzerprofile + ECDH/OPK-Schlüssel |
+| `prekeys` | X3DH-Einmal-Prekeys |
+| `friends` | Freundschaftsbeziehungen |
+| `groups` / `group_members` | Gruppenchats + Mitglieder |
+| `messages` | Verschlüsselte Nachrichten |
+| `moments` / `moment_images` | Soziale Beiträge + Bilder |
+| `moment_likes` / `moment_comments` | Likes + Kommentare |
+| `push_subscriptions` | Web Push (VAPID) |
+| `onesignal_players` | OneSignal-Geräte (Median.co) |
---
-## Database Schema
+## Umgebungsvariablen
-11 tables, auto-created on first server startup (`CREATE TABLE IF NOT EXISTS`):
-
-| Table | Purpose |
-|-------|---------|
-| `users` | User profiles + ECDH/OPK public keys |
-| `prekeys` | X3DH one-time prekey pool |
-| `friends` | Friendship relationships (pending / accepted / blocked) |
-| `groups` / `group_members` | Group chats + membership (incl. mute/DND status) |
-| `messages` | Encrypted payloads (offline buffer, deletable after delivery) |
-| `moments` | Social posts (text ≤ 1024 chars) |
-| `moment_images` | Post images (up to 9 per post) |
-| `moment_likes` | Likes (unique per user per post) |
-| `moment_comments` | Comments (≤ 512 chars each) |
-| `push_subscriptions` | Web Push subscriptions (VAPID) |
-| `onesignal_players` | OneSignal device registrations (Median.co) |
+| Variable | Beschreibung | Standard |
+|----------|-------------|----------|
+| `PORT` | Server-Port | `3000` |
+| `JWT_SECRET` | JWT-Signaturschlüssel (**in Produktion ändern**) | dev_secret |
+| `DB_HOST`/`DB_PASS`/`DB_NAME` | MySQL-Verbindung | — |
+| `REDIS_HOST`/`REDIS_PASS` | Redis-Verbindung | — |
+| `R2_ACCOUNT_ID` | Cloudflare-Konto-ID | — |
+| `R2_ACCESS_KEY_ID` | R2-API-Zugriffsschlüssel | — |
+| `R2_SECRET_ACCESS_KEY` | R2-API-Geheimschlüssel | — |
+| `R2_BUCKET` | R2-Bucket-Name | — |
+| `R2_PUBLIC_URL` | Öffentliche R2-URL (optional) | — |
+| `CF_CALLS_APP_ID` | Calls App ID (optional) | — |
+| `CF_CALLS_APP_SECRET` | Calls Secret (optional) | — |
+| `VAPID_PUBLIC_KEY` | VAPID öffentlicher Schlüssel (optional) | — |
+| `VAPID_PRIVATE_KEY` | VAPID privater Schlüssel (optional) | — |
+| `ONESIGNAL_APP_ID` | OneSignal App ID (optional) | — |
+| `ONESIGNAL_REST_KEY` | OneSignal REST Key (optional) | — |
---
-## Security Model
-
-```
-On Registration:
- Device generates IK (Identity Key) + SPK (Signed PreKey) + 10× OPK (One-Time PreKeys)
- Public keys are uploaded; private keys stay on-device (four-tier persistence)
-
-On Each Message:
- Sender fetches recipient's IK public key
- Generates a fresh ephemeral ECDH keypair (per-message)
- X25519 ECDH → shared secret → XSalsa20-Poly1305 encrypt
- Ephemeral public key sent in message header; destroyed after use
-
-What the Server Sees:
- ✅ Ciphertext blob + routing metadata (sender / recipient UUID, timestamps)
- ❌ Plaintext / private keys / ephemeral keys / call content
-```
-
----
-
-## Environment Variables
-
-| Variable | Description | Default |
-|----------|-------------|---------|
-| `PORT` | Server port | `3000` |
-| `JWT_SECRET` | JWT signing key (**change in production**) | dev_secret |
-| `DB_HOST` / `DB_PASS` / `DB_NAME` | MySQL connection | — |
-| `REDIS_HOST` / `REDIS_PASS` | Redis connection | — |
-| `R2_ACCOUNT_ID` | Cloudflare account ID | — |
-| `R2_ACCESS_KEY_ID` | R2 API token access key | — |
-| `R2_SECRET_ACCESS_KEY` | R2 API token secret key | — |
-| `R2_BUCKET` | R2 bucket name | — |
-| `R2_PUBLIC_URL` | R2 public base URL (optional) — enables direct CDN links | — |
-| `CF_CALLS_APP_ID` | Cloudflare Calls App ID (optional) | — |
-| `CF_CALLS_APP_SECRET` | Cloudflare Calls App Secret (optional) | — |
-| `VAPID_PUBLIC_KEY` | Web Push VAPID public key (optional) | — |
-| `VAPID_PRIVATE_KEY` | Web Push VAPID private key (optional) | — |
-| `VAPID_SUBJECT` | VAPID contact email (optional) | `mailto:admin@paperphone.app` |
-| `ONESIGNAL_APP_ID` | OneSignal App ID (optional, for Median.co) | — |
-| `ONESIGNAL_REST_KEY` | OneSignal REST API Key (optional) | — |
-
----
-
-## License
+## Lizenz
MIT © PaperPhone Contributors
diff --git a/README_ES.md b/README_ES.md
index e4cef23..2129c04 100644
--- a/README_ES.md
+++ b/README_ES.md
@@ -10,367 +10,173 @@ Una aplicación de mensajería instantánea cifrada de extremo a extremo estilo
---
-## Features
-| Feature | Description |
+## Características
+
+| Función | Descripción |
|---------|-------------|
-| 🔐 End-to-End Encryption | Stateless ECDH + XSalsa20-Poly1305 — ephemeral keys per message, forward secrecy |
-| 🗑️ Zero-Knowledge Server | Server stores only ciphertext; private keys never leave the device |
-| 📹 Video & Voice Calls | WebRTC P2P (1:1) + Mesh (group), Cloudflare TURN for NAT traversal |
-| 👥 Group Chat | Up to 2000 members, plain-text messages (no encryption), Do Not Disturb mode, member management |
-| ⏱️ Auto-Delete Messages | 5 tiers (never / 1 day / 3 days / 1 week / 1 month), settable by either party in DMs, owner-only in groups |
-| 🔔 Push Notifications | Web Push (VAPID) + OneSignal dual-channel — reach users even when offline |
-| 🌐 Multi-Language | Chinese, English, Japanese, Korean, French, German, Russian, Spanish — auto-detect + manual switch |
-| 📱 iOS — No Enterprise Cert | PWA via Safari "Add to Home Screen", works permanently without Apple signing |
-| 💬 Rich Messaging | Text, images, voice messages, 64-emoji panel, delivery receipts, typing indicators |
-| 🌐 Moments | WeChat-style social feed: text + up to 9 photos, likes (friend avatars), comments, tag-based visibility control |
-| 🏷️ Friend Tags | Assign multiple tags to friends (12-color preset palette), filter contacts by tag |
-| 🗂️ R2 Object Storage | Cloudflare R2 for image/voice files — optional public CDN URL |
-| 🏗️ Self-Hostable | Docker Compose one-command deployment; Node.js + Redis multi-node ready |
+| 🔐 Cifrado E2E | ECDH sin estado + XSalsa20-Poly1305 — claves efímeras por mensaje, secreto hacia adelante |
+| 🗝️ Servidor de conocimiento cero | El servidor solo almacena texto cifrado; las claves privadas nunca salen del dispositivo |
+| 📹 Videollamadas/voz | WebRTC P2P (1:1) + Mesh (grupo), Cloudflare TURN para atravesar NAT |
+| 👥 Chat grupal | Hasta 2000 miembros, mensajes en texto plano, modo No molestar, gestión de miembros |
+| ⏱️ Eliminación automática | 5 niveles (nunca/1d/3d/1sem/1mes), en DMs ambas partes, en grupos solo propietario |
+| 🔔 Notificaciones push | Web Push (VAPID) + OneSignal canal doble |
+| 🌐 Multilingüe | ZH/EN/JA/KO/FR/DE/RU/ES — detección automática + selección manual |
+| 📱 iOS sin certificado empresarial | PWA vía Safari "Añadir a inicio", sin firma de Apple |
+| 💬 Mensajería rica | Texto, imágenes, mensajes de voz, 64 emojis, confirmaciones de lectura |
+| 🌐 Momentos | Feed social: texto + hasta 9 fotos, likes (avatares de amigos), comentarios, visibilidad por etiquetas |
+| 🏷️ Etiquetas de amigos | Múltiples etiquetas por amigo (paleta de 12 colores), filtrar contactos por etiqueta |
+| 🗂️ Almacenamiento R2 | Cloudflare R2 para imágenes/audio — URL CDN opcional |
+| 🏗️ Auto-alojable | Despliegue Docker Compose en un comando |
---
-## Tech Stack
+## Stack tecnológico
```
Backend (server/)
Node.js 20 + Express + ws
- MySQL 8.0 — users, messages (persisted ciphertext)
- Redis — online presence + cross-node routing
- Cloudflare R2 — image/voice file storage (S3-compatible API)
- JWT + bcrypt authentication
+ MySQL 8.0 — usuarios, mensajes (texto cifrado)
+ Redis — presencia en línea + enrutamiento entre nodos
+ Cloudflare R2 — almacenamiento de archivos (API compatible S3)
+ Autenticación JWT + bcrypt
Frontend (client/)
- Native HTML + Vanilla JS (ESM, no bundler required)
+ HTML nativo + Vanilla JS (ESM, sin bundler)
libsodium-wrappers (WebAssembly — Curve25519 / XSalsa20-Poly1305)
- WebRTC API — video / voice calls
+ API WebRTC — videollamadas / llamadas de voz
PWA: manifest.json + Service Worker
-Cryptographic Layer
- Stateless ECDH + XSalsa20-Poly1305 — ephemeral keypair per message
- Four-tier key persistence: memory → localStorage → sessionStorage → IndexedDB
- All private keys stored on-device only — never sent to the server
+Capa criptográfica
+ ECDH sin estado + XSalsa20-Poly1305 — par de claves efímeras por mensaje
+ Persistencia de claves en 4 niveles: memoria → localStorage → sessionStorage → IndexedDB
+ Todas las claves privadas solo en el dispositivo — nunca se envían al servidor
```
---
-## Quick Start
+## Inicio rápido
-### Option 0: Zeabur One-Click Cloud Deploy
+### Opción 0: Zeabur — Despliegue en la nube con un clic
[](https://zeabur.com/templates/P2J7Y3?referralCode=619dev)
> [!NOTE]
-> One manual step is required after the template deploys, otherwise login/register won't work:
-> 1. Go to Zeabur Console → **server service** → Environment Variables → copy the value of `ZEABUR_WEB_URL` (e.g. `http://10.43.x.x:3000`)
-> 2. Go to **client service** → Environment Variables → add variable `SERVER_URL` = the value copied above
-> 3. Restart the client service
+> Se requiere un paso manual después del despliegue:
+> 1. Consola Zeabur → servicio **server** → Variables de entorno → copiar el valor de `ZEABUR_WEB_URL`
+> 2. Servicio **client** → Variables de entorno → agregar `SERVER_URL` = valor copiado
+> 3. Reiniciar el servicio client
-**Known notes:**
-- On first startup, the server automatically creates all database tables (`CREATE TABLE IF NOT EXISTS`) — no manual SQL import needed
-- Redis runs without a password inside the cluster (intra-cluster network isolation is sufficient)
-- If MySQL access is denied, manually set `DB_PASS` on the server service to the value of `MYSQL_ROOT_PASSWORD` from the MySQL service
-- To get the **internal IP** of any service container, open that service's Terminal in the Zeabur console and run:
- ```bash
- hostname -i
- ```
-
----
-
-### Option 1: Docker Compose (Recommended — no local build needed)
+### Opción 1: Docker Compose (recomendado)
```bash
-# Clone the repository
git clone && cd paperphone
-
-# Copy and edit environment variables
cp server/.env.example server/.env
-# Fill in: DB_PASS / JWT_SECRET / CF_CALLS_APP_ID etc.
-
-# Pull images and start everything
+# Editar: DB_PASS / JWT_SECRET / R2_* etc.
docker compose up -d
-
-# Check service status
-docker compose ps
-
-# Open in browser
open http://localhost
```
-> Pre-built images on Docker Hub:
-> - `facilisvelox/paperphone-client:latest`
-> - `facilisvelox/paperphone-server:latest`
->
-> **Note**: The server automatically initialises the database schema on first startup — no manual SQL import required.
+> Imágenes Docker Hub: `facilisvelox/paperphone-client:latest` y `facilisvelox/paperphone-server:latest`
-### Option 2: Manual Local Start
-
-#### 1. Prepare the environment
+### Opción 2: Inicio local manual
```bash
-# Copy and edit environment variables
-cp server/.env.example server/.env
-# Fill in DB_HOST / DB_PASS / REDIS_HOST / R2_* etc.
+# Backend
+cd server && npm install && npm run dev # → http://localhost:3000
-# Note: the server auto-runs schema.sql on first startup
-```
-
-#### 2. Start the backend
-
-```bash
-cd server
-npm install
-npm run dev # → http://localhost:3000
-```
-
-#### 3. Start the frontend
-
-```bash
-npx serve client -p 8080
-# → http://localhost:8080
+# Frontend
+npx serve client -p 8080 # → http://localhost:8080
```
---
-## Video Call Configuration
-
-Video and voice calls use WebRTC P2P and work out of the box on the same LAN. For cross-network calls, a TURN server is required for NAT traversal.
-
-### Using Cloudflare TURN (Recommended)
-
-1. Log in to [Cloudflare Dashboard](https://dash.cloudflare.com) → **Workers & Pages** → **Calls** → create an App
-2. Copy the **App ID** and **App Secret** (Token Key)
-3. Add them to `server/.env`:
+## Configuración de videollamadas — Cloudflare TURN
```env
CF_CALLS_APP_ID=your_app_id_here
CF_CALLS_APP_SECRET=your_app_secret_here
```
-4. Restart the backend — TURN credentials are fetched automatically per call session (TTL: 86 400 s)
+| Tipo | Transporte | Recomendado para |
+|------|-----------|------------------|
+| Video 1:1 | WebRTC P2P + TURN | Todos los escenarios |
+| Voz 1:1 | WebRTC P2P + TURN | Todos los escenarios |
+| Llamada grupal | WebRTC Mesh | Hasta 6 participantes |
-> **Without credentials**: the server falls back to STUN only (Google + Cloudflare public STUN). Calls work on LAN without any extra configuration.
-
-### Call Types
-
-| Type | Transport | Recommended for |
-|------|-----------|-----------------|
-| 1:1 Video Call | WebRTC P2P + TURN | All scenarios |
-| 1:1 Voice Call | WebRTC P2P + TURN | All scenarios |
-| Group Video / Voice | WebRTC Mesh (full-mesh) | Up to 6 participants |
+> **Sin configuración**: solo STUN. Las llamadas en LAN funcionan sin configuración adicional.
---
-## Push Notification Configuration
+## Notificaciones push
-Offline message notifications are delivered through **two channels** for maximum delivery rate:
-
-| Channel | Platforms | Configuration |
-|---------|-----------|---------------|
-| Web Push (VAPID) | Browsers (Chrome/Edge/Firefox) + iOS PWA (Safari 16.4+) | VAPID keys |
-| OneSignal | Native Android/iOS apps via Median.co | OneSignal App ID + REST Key |
-
-### Configuring Web Push
-
-1. Generate VAPID keys (one-time):
+| Canal | Plataformas | Configuración |
+|-------|-------------|---------------|
+| Web Push | Navegadores + iOS PWA (Safari 16.4+) | Claves VAPID |
+| OneSignal | Apps nativas Median.co | App ID + REST Key |
```bash
-cd server
npx web-push generate-vapid-keys
```
-2. Add to `server/.env`:
+---
-```env
-VAPID_PUBLIC_KEY=your_public_key_here
-VAPID_PRIVATE_KEY=your_private_key_here
-VAPID_SUBJECT=mailto:admin@your-domain.com
-```
+## iOS — Instalación permanente sin certificado
-3. Restart the server — users can enable notifications from the Settings page
-
-> **iOS users** must first "Add to Home Screen" (PWA), and only iOS 16.4+ is supported.
-
-### Configuring OneSignal (Median.co Native Apps)
-
-1. Create an app on [OneSignal Dashboard](https://onesignal.com) and configure Firebase
-2. Enable OneSignal in Median.co and enter the App ID
-3. Add the OneSignal **App ID** and **REST API Key** to `server/.env`:
-
-```env
-ONESIGNAL_APP_ID=your_onesignal_app_id
-ONESIGNAL_REST_KEY=your_onesignal_rest_api_key
-```
-
-> **When not configured**: push notifications are silently disabled — all other features work normally.
+1. Desplegar en servidor HTTPS → 2. Abrir en Safari → 3. Compartir ⬆️ → 4. «Añadir a pantalla de inicio»
---
-## iOS — Permanent No-Cert Deployment
+## Modelo de seguridad
-1. Deploy to a server with an HTTPS domain (required for WebRTC and Web Crypto APIs)
-2. Open `https://your.domain.com` in **Safari**
-3. Tap the Share button ⬆️ at the bottom of the screen
-4. Select **Add to Home Screen** → **Add**
-
-The app behaves identically to a native app — no Apple enterprise certificate required, no expiry.
-
----
-
-## Production Deployment (Nginx)
-
-```nginx
-server {
- listen 443 ssl http2;
- server_name your.domain.com;
-
- ssl_certificate /path/to/cert.pem;
- ssl_certificate_key /path/to/key.pem;
-
- # Frontend static files
- location / {
- root /path/to/paperphone/client;
- try_files $uri /index.html;
- }
-
- # REST API
- location /api/ {
- proxy_pass http://localhost:3000;
- proxy_set_header Host $host;
- }
-
- # WebSocket (messaging + call signalling)
- location /ws {
- proxy_pass http://localhost:3000;
- proxy_http_version 1.1;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection "upgrade";
- proxy_read_timeout 3600s;
- }
-}
+```
+Registro: IK + SPK + 10×OPK generados localmente, claves públicas subidas
+Mensaje: ECDH efímero → X25519 → XSalsa20-Poly1305
+El servidor ve: ✅ texto cifrado + metadatos de enrutamiento ❌ texto plano / claves privadas
```
---
-## Project Structure
+## Esquema de base de datos
-```
-paperphone/
-├── docker-compose.yml
-├── server/
-│ ├── .env # Environment variables (incl. Cloudflare TURN keys)
-│ └── src/
-│ ├── app.js # Express application
-│ ├── routes/
-│ │ ├── auth.js # Register / Login (incl. X3DH public key upload)
-│ │ ├── users.js # User search / Prekey bundle download
-│ │ ├── friends.js # Friend requests / Accept (incl. offline push)
-│ │ ├── groups.js # Group management
-│ │ ├── messages.js # Historical messages (paginated ciphertext)
-│ │ ├── upload.js # Cloudflare R2 file upload
-│ │ ├── files.js # File proxy (when R2_PUBLIC_URL is not set)
-│ │ ├── moments.js # Moments feed (posts / likes / comments)
-│ │ ├── calls.js # TURN credential issuance
-│ │ └── push.js # Push subscription mgmt (Web Push + OneSignal)
-│ ├── services/
-│ │ ├── push.js # Web Push VAPID service
-│ │ └── onesignal.js # OneSignal REST API service
-│ └── ws/
-│ └── wsServer.js # WebSocket router (messages + call signalling + offline push)
-│
-└── client/
- ├── index.html # SPA entry + PWA meta + Median push bridge
- ├── manifest.json # PWA manifest
- ├── sw.js # Service Worker (offline cache + push notifications)
- └── src/
- ├── style.css # Premium design system (dark/light, glassmorphism)
- ├── app.js # Router + global state + incoming call listener
- ├── api.js # HTTP client
- ├── socket.js # WebSocket client (auto-reconnect)
- ├── i18n.js # Multi-language engine (zh / en / ja / ko / fr / de / ru / es)
- ├── services/
- │ ├── webrtc.js # WebRTC manager — CallManager class
- │ └── pushNotification.js # Push subscription mgmt (Web Push + Median bridge)
- ├── crypto/
- │ ├── ratchet.js # X3DH + Double Ratchet + ML-KEM-768
- │ └── keystore.js # IndexedDB private key store
- ├── pages/
- │ ├── login.js # Login / Register (key generation, language picker)
- │ ├── chats.js # Chat list
- │ ├── chat.js # Chat window (E2E encryption, call buttons)
- │ ├── groups.js # Group list (create group, search)
- │ ├── groupInfo.js # Group info (member management, DND, leave/disband)
- │ ├── contacts.js # Contacts (friend requests, online status)
- │ ├── discover.js # Discover page
- │ ├── profile.js # Me / Settings (language, fingerprint, notifications, PWA)
- │ └── call.js # Call UI (incoming / active / multi-party video)
-```
+11 tablas, creadas automáticamente en el primer inicio:
+
+| Tabla | Propósito |
+|-------|-----------|
+| `users` | Perfiles + claves públicas ECDH/OPK |
+| `prekeys` | Pre-claves X3DH de un solo uso |
+| `friends` | Relaciones de amistad |
+| `groups` / `group_members` | Grupos + miembros |
+| `messages` | Mensajes cifrados |
+| `moments` / `moment_images` | Publicaciones + imágenes |
+| `moment_likes` / `moment_comments` | Likes + comentarios |
+| `push_subscriptions` | Web Push (VAPID) |
+| `onesignal_players` | Dispositivos OneSignal |
---
-## Database Schema
+## Variables de entorno
-11 tables, auto-created on first server startup (`CREATE TABLE IF NOT EXISTS`):
-
-| Table | Purpose |
-|-------|---------|
-| `users` | User profiles + ECDH/OPK public keys |
-| `prekeys` | X3DH one-time prekey pool |
-| `friends` | Friendship relationships (pending / accepted / blocked) |
-| `groups` / `group_members` | Group chats + membership (incl. mute/DND status) |
-| `messages` | Encrypted payloads (offline buffer, deletable after delivery) |
-| `moments` | Social posts (text ≤ 1024 chars) |
-| `moment_images` | Post images (up to 9 per post) |
-| `moment_likes` | Likes (unique per user per post) |
-| `moment_comments` | Comments (≤ 512 chars each) |
-| `push_subscriptions` | Web Push subscriptions (VAPID) |
-| `onesignal_players` | OneSignal device registrations (Median.co) |
+| Variable | Descripción | Predeterminado |
+|----------|-------------|----------------|
+| `PORT` | Puerto del servidor | `3000` |
+| `JWT_SECRET` | Clave de firma JWT (**cambiar en producción**) | dev_secret |
+| `DB_HOST`/`DB_PASS`/`DB_NAME` | Conexión MySQL | — |
+| `REDIS_HOST`/`REDIS_PASS` | Conexión Redis | — |
+| `R2_ACCOUNT_ID` | ID de cuenta Cloudflare | — |
+| `R2_ACCESS_KEY_ID` | Clave de acceso R2 API | — |
+| `R2_SECRET_ACCESS_KEY` | Clave secreta R2 API | — |
+| `R2_BUCKET` | Nombre del bucket R2 | — |
+| `R2_PUBLIC_URL` | URL pública R2 (opc.) | — |
+| `CF_CALLS_APP_ID` | Calls App ID (opc.) | — |
+| `CF_CALLS_APP_SECRET` | Calls Secret (opc.) | — |
+| `VAPID_PUBLIC_KEY` | Clave pública VAPID (opc.) | — |
+| `VAPID_PRIVATE_KEY` | Clave privada VAPID (opc.) | — |
+| `ONESIGNAL_APP_ID` | OneSignal App ID (opc.) | — |
+| `ONESIGNAL_REST_KEY` | OneSignal REST Key (opc.) | — |
---
-## Security Model
-
-```
-On Registration:
- Device generates IK (Identity Key) + SPK (Signed PreKey) + 10× OPK (One-Time PreKeys)
- Public keys are uploaded; private keys stay on-device (four-tier persistence)
-
-On Each Message:
- Sender fetches recipient's IK public key
- Generates a fresh ephemeral ECDH keypair (per-message)
- X25519 ECDH → shared secret → XSalsa20-Poly1305 encrypt
- Ephemeral public key sent in message header; destroyed after use
-
-What the Server Sees:
- ✅ Ciphertext blob + routing metadata (sender / recipient UUID, timestamps)
- ❌ Plaintext / private keys / ephemeral keys / call content
-```
-
----
-
-## Environment Variables
-
-| Variable | Description | Default |
-|----------|-------------|---------|
-| `PORT` | Server port | `3000` |
-| `JWT_SECRET` | JWT signing key (**change in production**) | dev_secret |
-| `DB_HOST` / `DB_PASS` / `DB_NAME` | MySQL connection | — |
-| `REDIS_HOST` / `REDIS_PASS` | Redis connection | — |
-| `R2_ACCOUNT_ID` | Cloudflare account ID | — |
-| `R2_ACCESS_KEY_ID` | R2 API token access key | — |
-| `R2_SECRET_ACCESS_KEY` | R2 API token secret key | — |
-| `R2_BUCKET` | R2 bucket name | — |
-| `R2_PUBLIC_URL` | R2 public base URL (optional) — enables direct CDN links | — |
-| `CF_CALLS_APP_ID` | Cloudflare Calls App ID (optional) | — |
-| `CF_CALLS_APP_SECRET` | Cloudflare Calls App Secret (optional) | — |
-| `VAPID_PUBLIC_KEY` | Web Push VAPID public key (optional) | — |
-| `VAPID_PRIVATE_KEY` | Web Push VAPID private key (optional) | — |
-| `VAPID_SUBJECT` | VAPID contact email (optional) | `mailto:admin@paperphone.app` |
-| `ONESIGNAL_APP_ID` | OneSignal App ID (optional, for Median.co) | — |
-| `ONESIGNAL_REST_KEY` | OneSignal REST API Key (optional) | — |
-
----
-
-## License
+## Licencia
MIT © PaperPhone Contributors
diff --git a/README_FR.md b/README_FR.md
index 8538958..c199b27 100644
--- a/README_FR.md
+++ b/README_FR.md
@@ -10,367 +10,147 @@ Une application de messagerie instantanée chiffrée de bout en bout, style WeCh
---
-## Features
-| Feature | Description |
-|---------|-------------|
-| 🔐 End-to-End Encryption | Stateless ECDH + XSalsa20-Poly1305 — ephemeral keys per message, forward secrecy |
-| 🗑️ Zero-Knowledge Server | Server stores only ciphertext; private keys never leave the device |
-| 📹 Video & Voice Calls | WebRTC P2P (1:1) + Mesh (group), Cloudflare TURN for NAT traversal |
-| 👥 Group Chat | Up to 2000 members, plain-text messages (no encryption), Do Not Disturb mode, member management |
-| ⏱️ Auto-Delete Messages | 5 tiers (never / 1 day / 3 days / 1 week / 1 month), settable by either party in DMs, owner-only in groups |
-| 🔔 Push Notifications | Web Push (VAPID) + OneSignal dual-channel — reach users even when offline |
-| 🌐 Multi-Language | Chinese, English, Japanese, Korean, French, German, Russian, Spanish — auto-detect + manual switch |
-| 📱 iOS — No Enterprise Cert | PWA via Safari "Add to Home Screen", works permanently without Apple signing |
-| 💬 Rich Messaging | Text, images, voice messages, 64-emoji panel, delivery receipts, typing indicators |
-| 🌐 Moments | WeChat-style social feed: text + up to 9 photos, likes (friend avatars), comments, tag-based visibility control |
-| 🏷️ Friend Tags | Assign multiple tags to friends (12-color preset palette), filter contacts by tag |
-| 🗂️ R2 Object Storage | Cloudflare R2 for image/voice files — optional public CDN URL |
-| 🏗️ Self-Hostable | Docker Compose one-command deployment; Node.js + Redis multi-node ready |
+## Fonctionnalités
+
+| Fonction | Description |
+|----------|-------------|
+| 🔐 Chiffrement E2E | ECDH sans état + XSalsa20-Poly1305 — clés éphémères par message, secret de transmission |
+| 🗝️ Serveur à connaissance nulle | Le serveur ne stocke que le texte chiffré ; les clés privées ne quittent jamais l'appareil |
+| 📹 Appels vidéo/audio | WebRTC P2P (1:1) + Mesh (groupe), Cloudflare TURN pour traversée NAT |
+| 👥 Chat de groupe | Jusqu'à 2000 membres, messages en texte brut, mode Ne pas déranger |
+| ⏱️ Suppression auto | 5 niveaux (jamais/1j/3j/1sem/1mois) |
+| 🔔 Notifications push | Web Push (VAPID) + OneSignal double canal |
+| 🌐 Multilingue | 中/EN/日/한/FR/DE/RU/ES — détection auto + sélection manuelle |
+| 📱 iOS PWA | Safari « Ajouter à l'écran d'accueil », sans certificat Apple |
+| 💬 Messagerie riche | Texte, images, audio, 64 émojis, accusés de réception |
+| 🌐 Moments | Fil social : texte + 9 photos, likes (avatars), commentaires, visibilité par tags |
+| 🏷️ Tags d'amis | Plusieurs tags par ami (12 couleurs), filtrage des contacts |
+| 🗂️ Stockage R2 | Cloudflare R2 pour images/audio — CDN optionnel |
+| 🏗️ Auto-hébergeable | Docker Compose en une commande |
---
-## Tech Stack
+## Stack technique
```
Backend (server/)
Node.js 20 + Express + ws
- MySQL 8.0 — users, messages (persisted ciphertext)
- Redis — online presence + cross-node routing
- Cloudflare R2 — image/voice file storage (S3-compatible API)
- JWT + bcrypt authentication
+ MySQL 8.0 — utilisateurs, messages chiffrés
+ Redis — présence en ligne + routage inter-nœuds
+ Cloudflare R2 — stockage fichiers (API S3)
+ JWT + bcrypt
Frontend (client/)
- Native HTML + Vanilla JS (ESM, no bundler required)
- libsodium-wrappers (WebAssembly — Curve25519 / XSalsa20-Poly1305)
- WebRTC API — video / voice calls
- PWA: manifest.json + Service Worker
+ HTML natif + Vanilla JS (ESM, sans bundler)
+ libsodium-wrappers (WebAssembly)
+ WebRTC API — appels vidéo/audio
+ PWA : manifest.json + Service Worker
-Cryptographic Layer
- Stateless ECDH + XSalsa20-Poly1305 — ephemeral keypair per message
- Four-tier key persistence: memory → localStorage → sessionStorage → IndexedDB
- All private keys stored on-device only — never sent to the server
+Cryptographie
+ ECDH sans état + XSalsa20-Poly1305 — clés éphémères par message
+ Persistance 4 niveaux : mémoire → localStorage → sessionStorage → IndexedDB
+ Clés privées uniquement sur l'appareil
```
---
-## Quick Start
+## Démarrage rapide
-### Option 0: Zeabur One-Click Cloud Deploy
+### Option 0 : Zeabur en un clic
[](https://zeabur.com/templates/P2J7Y3?referralCode=619dev)
> [!NOTE]
-> One manual step is required after the template deploys, otherwise login/register won't work:
-> 1. Go to Zeabur Console → **server service** → Environment Variables → copy the value of `ZEABUR_WEB_URL` (e.g. `http://10.43.x.x:3000`)
-> 2. Go to **client service** → Environment Variables → add variable `SERVER_URL` = the value copied above
-> 3. Restart the client service
+> Étape manuelle requise après déploiement :
+> 1. Console Zeabur → **server** → Variables → copier `ZEABUR_WEB_URL`
+> 2. **client** → Variables → ajouter `SERVER_URL` = valeur copiée
+> 3. Redémarrer le client
-**Known notes:**
-- On first startup, the server automatically creates all database tables (`CREATE TABLE IF NOT EXISTS`) — no manual SQL import needed
-- Redis runs without a password inside the cluster (intra-cluster network isolation is sufficient)
-- If MySQL access is denied, manually set `DB_PASS` on the server service to the value of `MYSQL_ROOT_PASSWORD` from the MySQL service
-- To get the **internal IP** of any service container, open that service's Terminal in the Zeabur console and run:
- ```bash
- hostname -i
- ```
-
----
-
-### Option 1: Docker Compose (Recommended — no local build needed)
+### Option 1 : Docker Compose (recommandé)
```bash
-# Clone the repository
git clone && cd paperphone
-
-# Copy and edit environment variables
cp server/.env.example server/.env
-# Fill in: DB_PASS / JWT_SECRET / CF_CALLS_APP_ID etc.
-
-# Pull images and start everything
+# Éditer les variables
docker compose up -d
-
-# Check service status
-docker compose ps
-
-# Open in browser
open http://localhost
```
-> Pre-built images on Docker Hub:
-> - `facilisvelox/paperphone-client:latest`
-> - `facilisvelox/paperphone-server:latest`
->
-> **Note**: The server automatically initialises the database schema on first startup — no manual SQL import required.
-
-### Option 2: Manual Local Start
-
-#### 1. Prepare the environment
+### Option 2 : Démarrage local
```bash
-# Copy and edit environment variables
-cp server/.env.example server/.env
-# Fill in DB_HOST / DB_PASS / REDIS_HOST / R2_* etc.
+# Backend
+cd server && npm install && npm run dev
-# Note: the server auto-runs schema.sql on first startup
-```
-
-#### 2. Start the backend
-
-```bash
-cd server
-npm install
-npm run dev # → http://localhost:3000
-```
-
-#### 3. Start the frontend
-
-```bash
+# Frontend
npx serve client -p 8080
-# → http://localhost:8080
```
---
-## Video Call Configuration
-
-Video and voice calls use WebRTC P2P and work out of the box on the same LAN. For cross-network calls, a TURN server is required for NAT traversal.
-
-### Using Cloudflare TURN (Recommended)
-
-1. Log in to [Cloudflare Dashboard](https://dash.cloudflare.com) → **Workers & Pages** → **Calls** → create an App
-2. Copy the **App ID** and **App Secret** (Token Key)
-3. Add them to `server/.env`:
+## Appels vidéo — Cloudflare TURN
```env
CF_CALLS_APP_ID=your_app_id_here
CF_CALLS_APP_SECRET=your_app_secret_here
```
-4. Restart the backend — TURN credentials are fetched automatically per call session (TTL: 86 400 s)
-
-> **Without credentials**: the server falls back to STUN only (Google + Cloudflare public STUN). Calls work on LAN without any extra configuration.
-
-### Call Types
-
-| Type | Transport | Recommended for |
-|------|-----------|-----------------|
-| 1:1 Video Call | WebRTC P2P + TURN | All scenarios |
-| 1:1 Voice Call | WebRTC P2P + TURN | All scenarios |
-| Group Video / Voice | WebRTC Mesh (full-mesh) | Up to 6 participants |
+> Sans configuration : STUN uniquement, appels LAN fonctionnent.
---
-## Push Notification Configuration
+## Notifications push
-Offline message notifications are delivered through **two channels** for maximum delivery rate:
-
-| Channel | Platforms | Configuration |
-|---------|-----------|---------------|
-| Web Push (VAPID) | Browsers (Chrome/Edge/Firefox) + iOS PWA (Safari 16.4+) | VAPID keys |
-| OneSignal | Native Android/iOS apps via Median.co | OneSignal App ID + REST Key |
-
-### Configuring Web Push
-
-1. Generate VAPID keys (one-time):
+| Canal | Plateformes | Configuration |
+|-------|-------------|---------------|
+| Web Push | Navigateurs + iOS PWA 16.4+ | Clés VAPID |
+| OneSignal | Apps Median.co | App ID + REST Key |
```bash
-cd server
npx web-push generate-vapid-keys
```
-2. Add to `server/.env`:
+---
-```env
-VAPID_PUBLIC_KEY=your_public_key_here
-VAPID_PRIVATE_KEY=your_private_key_here
-VAPID_SUBJECT=mailto:admin@your-domain.com
-```
+## iOS — Sans certificat
-3. Restart the server — users can enable notifications from the Settings page
-
-> **iOS users** must first "Add to Home Screen" (PWA), and only iOS 16.4+ is supported.
-
-### Configuring OneSignal (Median.co Native Apps)
-
-1. Create an app on [OneSignal Dashboard](https://onesignal.com) and configure Firebase
-2. Enable OneSignal in Median.co and enter the App ID
-3. Add the OneSignal **App ID** and **REST API Key** to `server/.env`:
-
-```env
-ONESIGNAL_APP_ID=your_onesignal_app_id
-ONESIGNAL_REST_KEY=your_onesignal_rest_api_key
-```
-
-> **When not configured**: push notifications are silently disabled — all other features work normally.
+1. Déployer avec HTTPS → 2. Ouvrir dans Safari → 3. Partager ⬆️ → 4. « Ajouter à l'écran d'accueil »
---
-## iOS — Permanent No-Cert Deployment
+## Sécurité
-1. Deploy to a server with an HTTPS domain (required for WebRTC and Web Crypto APIs)
-2. Open `https://your.domain.com` in **Safari**
-3. Tap the Share button ⬆️ at the bottom of the screen
-4. Select **Add to Home Screen** → **Add**
-
-The app behaves identically to a native app — no Apple enterprise certificate required, no expiry.
-
----
-
-## Production Deployment (Nginx)
-
-```nginx
-server {
- listen 443 ssl http2;
- server_name your.domain.com;
-
- ssl_certificate /path/to/cert.pem;
- ssl_certificate_key /path/to/key.pem;
-
- # Frontend static files
- location / {
- root /path/to/paperphone/client;
- try_files $uri /index.html;
- }
-
- # REST API
- location /api/ {
- proxy_pass http://localhost:3000;
- proxy_set_header Host $host;
- }
-
- # WebSocket (messaging + call signalling)
- location /ws {
- proxy_pass http://localhost:3000;
- proxy_http_version 1.1;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection "upgrade";
- proxy_read_timeout 3600s;
- }
-}
+```
+Inscription : IK + SPK + 10×OPK générés localement, clés publiques uploadées
+Message : ECDH éphémère → X25519 → XSalsa20-Poly1305
+Serveur voit : ✅ texte chiffré + métadonnées ❌ texte clair / clés privées
```
---
-## Project Structure
+## Variables d'environnement
-```
-paperphone/
-├── docker-compose.yml
-├── server/
-│ ├── .env # Environment variables (incl. Cloudflare TURN keys)
-│ └── src/
-│ ├── app.js # Express application
-│ ├── routes/
-│ │ ├── auth.js # Register / Login (incl. X3DH public key upload)
-│ │ ├── users.js # User search / Prekey bundle download
-│ │ ├── friends.js # Friend requests / Accept (incl. offline push)
-│ │ ├── groups.js # Group management
-│ │ ├── messages.js # Historical messages (paginated ciphertext)
-│ │ ├── upload.js # Cloudflare R2 file upload
-│ │ ├── files.js # File proxy (when R2_PUBLIC_URL is not set)
-│ │ ├── moments.js # Moments feed (posts / likes / comments)
-│ │ ├── calls.js # TURN credential issuance
-│ │ └── push.js # Push subscription mgmt (Web Push + OneSignal)
-│ ├── services/
-│ │ ├── push.js # Web Push VAPID service
-│ │ └── onesignal.js # OneSignal REST API service
-│ └── ws/
-│ └── wsServer.js # WebSocket router (messages + call signalling + offline push)
-│
-└── client/
- ├── index.html # SPA entry + PWA meta + Median push bridge
- ├── manifest.json # PWA manifest
- ├── sw.js # Service Worker (offline cache + push notifications)
- └── src/
- ├── style.css # Premium design system (dark/light, glassmorphism)
- ├── app.js # Router + global state + incoming call listener
- ├── api.js # HTTP client
- ├── socket.js # WebSocket client (auto-reconnect)
- ├── i18n.js # Multi-language engine (zh / en / ja / ko / fr / de / ru / es)
- ├── services/
- │ ├── webrtc.js # WebRTC manager — CallManager class
- │ └── pushNotification.js # Push subscription mgmt (Web Push + Median bridge)
- ├── crypto/
- │ ├── ratchet.js # X3DH + Double Ratchet + ML-KEM-768
- │ └── keystore.js # IndexedDB private key store
- ├── pages/
- │ ├── login.js # Login / Register (key generation, language picker)
- │ ├── chats.js # Chat list
- │ ├── chat.js # Chat window (E2E encryption, call buttons)
- │ ├── groups.js # Group list (create group, search)
- │ ├── groupInfo.js # Group info (member management, DND, leave/disband)
- │ ├── contacts.js # Contacts (friend requests, online status)
- │ ├── discover.js # Discover page
- │ ├── profile.js # Me / Settings (language, fingerprint, notifications, PWA)
- │ └── call.js # Call UI (incoming / active / multi-party video)
-```
+| Variable | Description | Défaut |
+|----------|-------------|--------|
+| `PORT` | Port serveur | `3000` |
+| `JWT_SECRET` | Clé JWT (**changer en prod**) | dev_secret |
+| `DB_HOST`/`DB_PASS`/`DB_NAME` | MySQL | — |
+| `REDIS_HOST`/`REDIS_PASS` | Redis | — |
+| `R2_ACCOUNT_ID` | Cloudflare ID | — |
+| `R2_ACCESS_KEY_ID` | Clé d'accès R2 | — |
+| `R2_SECRET_ACCESS_KEY` | Clé secrète R2 | — |
+| `R2_BUCKET` | Nom du bucket | — |
+| `R2_PUBLIC_URL` | URL publique (optionnel) | — |
+| `CF_CALLS_APP_ID` | Calls App ID (optionnel) | — |
+| `CF_CALLS_APP_SECRET` | Calls Secret (optionnel) | — |
+| `VAPID_PUBLIC_KEY` | Clé publique VAPID (optionnel) | — |
+| `VAPID_PRIVATE_KEY` | Clé privée VAPID (optionnel) | — |
+| `ONESIGNAL_APP_ID` | OneSignal ID (optionnel) | — |
+| `ONESIGNAL_REST_KEY` | OneSignal Key (optionnel) | — |
---
-## Database Schema
-
-11 tables, auto-created on first server startup (`CREATE TABLE IF NOT EXISTS`):
-
-| Table | Purpose |
-|-------|---------|
-| `users` | User profiles + ECDH/OPK public keys |
-| `prekeys` | X3DH one-time prekey pool |
-| `friends` | Friendship relationships (pending / accepted / blocked) |
-| `groups` / `group_members` | Group chats + membership (incl. mute/DND status) |
-| `messages` | Encrypted payloads (offline buffer, deletable after delivery) |
-| `moments` | Social posts (text ≤ 1024 chars) |
-| `moment_images` | Post images (up to 9 per post) |
-| `moment_likes` | Likes (unique per user per post) |
-| `moment_comments` | Comments (≤ 512 chars each) |
-| `push_subscriptions` | Web Push subscriptions (VAPID) |
-| `onesignal_players` | OneSignal device registrations (Median.co) |
-
----
-
-## Security Model
-
-```
-On Registration:
- Device generates IK (Identity Key) + SPK (Signed PreKey) + 10× OPK (One-Time PreKeys)
- Public keys are uploaded; private keys stay on-device (four-tier persistence)
-
-On Each Message:
- Sender fetches recipient's IK public key
- Generates a fresh ephemeral ECDH keypair (per-message)
- X25519 ECDH → shared secret → XSalsa20-Poly1305 encrypt
- Ephemeral public key sent in message header; destroyed after use
-
-What the Server Sees:
- ✅ Ciphertext blob + routing metadata (sender / recipient UUID, timestamps)
- ❌ Plaintext / private keys / ephemeral keys / call content
-```
-
----
-
-## Environment Variables
-
-| Variable | Description | Default |
-|----------|-------------|---------|
-| `PORT` | Server port | `3000` |
-| `JWT_SECRET` | JWT signing key (**change in production**) | dev_secret |
-| `DB_HOST` / `DB_PASS` / `DB_NAME` | MySQL connection | — |
-| `REDIS_HOST` / `REDIS_PASS` | Redis connection | — |
-| `R2_ACCOUNT_ID` | Cloudflare account ID | — |
-| `R2_ACCESS_KEY_ID` | R2 API token access key | — |
-| `R2_SECRET_ACCESS_KEY` | R2 API token secret key | — |
-| `R2_BUCKET` | R2 bucket name | — |
-| `R2_PUBLIC_URL` | R2 public base URL (optional) — enables direct CDN links | — |
-| `CF_CALLS_APP_ID` | Cloudflare Calls App ID (optional) | — |
-| `CF_CALLS_APP_SECRET` | Cloudflare Calls App Secret (optional) | — |
-| `VAPID_PUBLIC_KEY` | Web Push VAPID public key (optional) | — |
-| `VAPID_PRIVATE_KEY` | Web Push VAPID private key (optional) | — |
-| `VAPID_SUBJECT` | VAPID contact email (optional) | `mailto:admin@paperphone.app` |
-| `ONESIGNAL_APP_ID` | OneSignal App ID (optional, for Median.co) | — |
-| `ONESIGNAL_REST_KEY` | OneSignal REST API Key (optional) | — |
-
----
-
-## License
+## Licence
MIT © PaperPhone Contributors
diff --git a/README_JA.md b/README_JA.md
index 9cd0a33..adbe012 100644
--- a/README_JA.md
+++ b/README_JA.md
@@ -10,112 +10,113 @@ WeChatスタイルのエンドツーエンド暗号化インスタントメッ
---
-## Features
-| Feature | Description |
-|---------|-------------|
-| 🔐 End-to-End Encryption | Stateless ECDH + XSalsa20-Poly1305 — ephemeral keys per message, forward secrecy |
-| 🗑️ Zero-Knowledge Server | Server stores only ciphertext; private keys never leave the device |
-| 📹 Video & Voice Calls | WebRTC P2P (1:1) + Mesh (group), Cloudflare TURN for NAT traversal |
-| 👥 Group Chat | Up to 2000 members, plain-text messages (no encryption), Do Not Disturb mode, member management |
-| ⏱️ Auto-Delete Messages | 5 tiers (never / 1 day / 3 days / 1 week / 1 month), settable by either party in DMs, owner-only in groups |
-| 🔔 Push Notifications | Web Push (VAPID) + OneSignal dual-channel — reach users even when offline |
-| 🌐 Multi-Language | Chinese, English, Japanese, Korean, French, German, Russian, Spanish — auto-detect + manual switch |
-| 📱 iOS — No Enterprise Cert | PWA via Safari "Add to Home Screen", works permanently without Apple signing |
-| 💬 Rich Messaging | Text, images, voice messages, 64-emoji panel, delivery receipts, typing indicators |
-| 🌐 Moments | WeChat-style social feed: text + up to 9 photos, likes (friend avatars), comments, tag-based visibility control |
-| 🏷️ Friend Tags | Assign multiple tags to friends (12-color preset palette), filter contacts by tag |
-| 🗂️ R2 Object Storage | Cloudflare R2 for image/voice files — optional public CDN URL |
-| 🏗️ Self-Hostable | Docker Compose one-command deployment; Node.js + Redis multi-node ready |
+## 機能一覧
+
+| 機能 | 説明 |
+|------|------|
+| 🔐 エンドツーエンド暗号化 | ステートレス ECDH + XSalsa20-Poly1305 — メッセージごとの一時鍵、前方秘匿性 |
+| 🗝️ ゼロ知識サーバー | サーバーは暗号文のみ保存。秘密鍵はデバイスから離れません |
+| 📹 ビデオ/音声通話 | WebRTC P2P(1:1)+ Mesh(グループ)、Cloudflare TURN による NAT トラバーサル |
+| 👥 グループチャット | 最大2000人、プレーンテキストメッセージ(暗号化なし)、通知オフモード、メンバー管理 |
+| ⏱️ メッセージ自動削除 | 5段階(なし/1日/3日/1週間/1ヶ月)、DM は双方設定可、グループはオーナーのみ |
+| 🔔 プッシュ通知 | Web Push (VAPID) + OneSignal デュアルチャネル — オフラインでも通知 |
+| 🌐 多言語対応 | 中国語・英語・日本語・韓国語・フランス語・ドイツ語・ロシア語・スペイン語(自動検出+手動切替) |
+| 📱 iOS 永久署名不要 | PWA — Safari「ホーム画面に追加」でエンタープライズ証明書なしで利用可能 |
+| 💬 リッチメッセージ | テキスト・画像・音声・絵文字パネル(64種)・既読確認 |
+| 🌐 モーメンツ | テキスト+最大9枚写真、いいね(友達アバター表示)、コメント、タグベースの公開範囲制御 |
+| 🏷️ フレンドタグ | 友達に複数タグを設定(12色プリセット)、タグ別に連絡先をフィルタリング |
+| 🗂️ R2 オブジェクトストレージ | Cloudflare R2 で画像/音声ファイルを保存 — オプションの CDN 直リンク |
+| 🏗️ セルフホスト対応 | Docker Compose ワンコマンドデプロイ、Node.js + Redis マルチノード対応 |
---
-## Tech Stack
+## 技術スタック
```
-Backend (server/)
+バックエンド (server/)
Node.js 20 + Express + ws
- MySQL 8.0 — users, messages (persisted ciphertext)
- Redis — online presence + cross-node routing
- Cloudflare R2 — image/voice file storage (S3-compatible API)
- JWT + bcrypt authentication
+ MySQL 8.0 — ユーザー・メッセージの永続化
+ Redis — オンラインプレゼンス+クロスノードルーティング
+ Cloudflare R2 — 画像/音声ファイルストレージ(S3互換API)
+ JWT + bcrypt 認証
-Frontend (client/)
- Native HTML + Vanilla JS (ESM, no bundler required)
- libsodium-wrappers (WebAssembly — Curve25519 / XSalsa20-Poly1305)
- WebRTC API — video / voice calls
+フロントエンド (client/)
+ ネイティブ HTML + Vanilla JS(ESM、バンドラー不要)
+ libsodium-wrappers(WebAssembly — Curve25519 / XSalsa20-Poly1305)
+ WebRTC API — ビデオ/音声通話
PWA: manifest.json + Service Worker
-Cryptographic Layer
- Stateless ECDH + XSalsa20-Poly1305 — ephemeral keypair per message
- Four-tier key persistence: memory → localStorage → sessionStorage → IndexedDB
- All private keys stored on-device only — never sent to the server
+暗号化レイヤー
+ ステートレス ECDH + XSalsa20-Poly1305 — メッセージごとの一時 ECDH 鍵ペア
+ 秘密鍵4層永続化: メモリ → localStorage → sessionStorage → IndexedDB
+ 秘密鍵はデバイスにのみ保存、サーバーに送信されることはありません
```
---
-## Quick Start
+## クイックスタート
-### Option 0: Zeabur One-Click Cloud Deploy
+### 方法0:Zeabur ワンクリッククラウドデプロイ
[](https://zeabur.com/templates/P2J7Y3?referralCode=619dev)
> [!NOTE]
-> One manual step is required after the template deploys, otherwise login/register won't work:
-> 1. Go to Zeabur Console → **server service** → Environment Variables → copy the value of `ZEABUR_WEB_URL` (e.g. `http://10.43.x.x:3000`)
-> 2. Go to **client service** → Environment Variables → add variable `SERVER_URL` = the value copied above
-> 3. Restart the client service
+> テンプレートのデプロイ後、1つの手動手順が必要です。これを行わないとログイン/登録が機能しません:
+> 1. Zeabur コンソール → **server サービス** → 環境変数 → `ZEABUR_WEB_URL` の値をコピー(例:`http://10.43.x.x:3000`)
+> 2. **client サービス** → 環境変数 → 変数 `SERVER_URL` を追加 = 上記でコピーした値
+> 3. client サービスを再起動
-**Known notes:**
-- On first startup, the server automatically creates all database tables (`CREATE TABLE IF NOT EXISTS`) — no manual SQL import needed
-- Redis runs without a password inside the cluster (intra-cluster network isolation is sufficient)
-- If MySQL access is denied, manually set `DB_PASS` on the server service to the value of `MYSQL_ROOT_PASSWORD` from the MySQL service
-- To get the **internal IP** of any service container, open that service's Terminal in the Zeabur console and run:
+**既知の注意事項:**
+- 初回起動時、サーバーは自動的にすべてのデータベーステーブルを作成します(`CREATE TABLE IF NOT EXISTS`)— SQL の手動インポートは不要
+- Redis はクラスター内でパスワードなしで動作します
+- MySQL アクセスが拒否された場合、server サービスの `DB_PASS` を MySQL サービスの `MYSQL_ROOT_PASSWORD` に手動設定してください
+- サービスコンテナの**内部IP**を取得するには、Zeabur コンソールで該当サービスのターミナルを開き、以下を実行:
```bash
hostname -i
```
---
-### Option 1: Docker Compose (Recommended — no local build needed)
+### 方法1:Docker Compose(推奨 — ローカルビルド不要)
```bash
-# Clone the repository
+# リポジトリをクローン
git clone && cd paperphone
-# Copy and edit environment variables
+# 環境変数をコピーして編集
cp server/.env.example server/.env
-# Fill in: DB_PASS / JWT_SECRET / CF_CALLS_APP_ID etc.
+# DB_PASS / JWT_SECRET / R2_* などを編集
-# Pull images and start everything
+# イメージを取得して起動
docker compose up -d
-# Check service status
+# サービスステータスを確認
docker compose ps
-# Open in browser
+# ブラウザで開く
open http://localhost
```
-> Pre-built images on Docker Hub:
+> Docker Hub のビルド済みイメージ:
> - `facilisvelox/paperphone-client:latest`
> - `facilisvelox/paperphone-server:latest`
>
-> **Note**: The server automatically initialises the database schema on first startup — no manual SQL import required.
+> **注意**:サーバーは初回起動時に自動的にデータベーススキーマを初期化します — SQL の手動インポートは不要です。
-### Option 2: Manual Local Start
+### 方法2:ローカル手動起動
-#### 1. Prepare the environment
+#### 1. 環境を準備
```bash
-# Copy and edit environment variables
+# 環境変数をコピーして編集
cp server/.env.example server/.env
-# Fill in DB_HOST / DB_PASS / REDIS_HOST / R2_* etc.
+# DB_HOST / DB_PASS / REDIS_HOST / R2_* などを入力
-# Note: the server auto-runs schema.sql on first startup
+# 注:サーバーは初回起動時に自動で schema.sql を実行します
```
-#### 2. Start the backend
+#### 2. バックエンドを起動
```bash
cd server
@@ -123,7 +124,7 @@ npm install
npm run dev # → http://localhost:3000
```
-#### 3. Start the frontend
+#### 3. フロントエンドを起動
```bash
npx serve client -p 8080
@@ -132,54 +133,54 @@ npx serve client -p 8080
---
-## Video Call Configuration
+## ビデオ通話の設定
-Video and voice calls use WebRTC P2P and work out of the box on the same LAN. For cross-network calls, a TURN server is required for NAT traversal.
+ビデオ通話と音声通話は WebRTC P2P を使用し、同一 LAN 内ではすぐに使えます。異なるネットワーク間の通話には、NAT トラバーサル用の TURN サーバーが必要です。
-### Using Cloudflare TURN (Recommended)
+### Cloudflare TURN の使用(推奨)
-1. Log in to [Cloudflare Dashboard](https://dash.cloudflare.com) → **Workers & Pages** → **Calls** → create an App
-2. Copy the **App ID** and **App Secret** (Token Key)
-3. Add them to `server/.env`:
+1. [Cloudflare Dashboard](https://dash.cloudflare.com) → **Workers & Pages** → **Calls** → App を作成
+2. **App ID** と **App Secret**(Token Key)をコピー
+3. `server/.env` に追加:
```env
CF_CALLS_APP_ID=your_app_id_here
CF_CALLS_APP_SECRET=your_app_secret_here
```
-4. Restart the backend — TURN credentials are fetched automatically per call session (TTL: 86 400 s)
+4. バックエンドを再起動 — TURN クレデンシャルは通話セッションごとに自動取得されます(TTL: 86,400秒)
-> **Without credentials**: the server falls back to STUN only (Google + Cloudflare public STUN). Calls work on LAN without any extra configuration.
+> **未設定時**:STUN のみにフォールバックします(Google + Cloudflare パブリック STUN)。LAN 内の通話は追加設定なしで動作します。
-### Call Types
+### 通話タイプ
-| Type | Transport | Recommended for |
-|------|-----------|-----------------|
-| 1:1 Video Call | WebRTC P2P + TURN | All scenarios |
-| 1:1 Voice Call | WebRTC P2P + TURN | All scenarios |
-| Group Video / Voice | WebRTC Mesh (full-mesh) | Up to 6 participants |
+| タイプ | 通信方式 | 推奨用途 |
+|--------|----------|----------|
+| 1:1 ビデオ通話 | WebRTC P2P + TURN | すべてのシナリオ |
+| 1:1 音声通話 | WebRTC P2P + TURN | すべてのシナリオ |
+| グループ通話 | WebRTC Mesh(フルメッシュ) | 最大6人 |
---
-## Push Notification Configuration
+## プッシュ通知の設定
-Offline message notifications are delivered through **two channels** for maximum delivery rate:
+オフラインメッセージ通知は**2つのチャネル**で配信され、配信率を最大化します:
-| Channel | Platforms | Configuration |
-|---------|-----------|---------------|
-| Web Push (VAPID) | Browsers (Chrome/Edge/Firefox) + iOS PWA (Safari 16.4+) | VAPID keys |
-| OneSignal | Native Android/iOS apps via Median.co | OneSignal App ID + REST Key |
+| チャネル | 対応プラットフォーム | 設定 |
+|----------|----------------------|------|
+| Web Push (VAPID) | ブラウザ (Chrome/Edge/Firefox) + iOS PWA (Safari 16.4+) | VAPID キー |
+| OneSignal | Median.co 経由のネイティブ Android/iOS アプリ | OneSignal App ID + REST Key |
-### Configuring Web Push
+### Web Push の設定
-1. Generate VAPID keys (one-time):
+1. VAPID キーを生成(1回のみ):
```bash
cd server
npx web-push generate-vapid-keys
```
-2. Add to `server/.env`:
+2. `server/.env` に追加:
```env
VAPID_PUBLIC_KEY=your_public_key_here
@@ -187,37 +188,37 @@ VAPID_PRIVATE_KEY=your_private_key_here
VAPID_SUBJECT=mailto:admin@your-domain.com
```
-3. Restart the server — users can enable notifications from the Settings page
+3. サーバーを再起動 — ユーザーは設定ページから通知を有効にできます
-> **iOS users** must first "Add to Home Screen" (PWA), and only iOS 16.4+ is supported.
+> **iOS ユーザー**は先に「ホーム画面に追加」(PWA)を行う必要があり、iOS 16.4以上のみサポートされます。
-### Configuring OneSignal (Median.co Native Apps)
+### OneSignal の設定(Median.co ネイティブアプリ)
-1. Create an app on [OneSignal Dashboard](https://onesignal.com) and configure Firebase
-2. Enable OneSignal in Median.co and enter the App ID
-3. Add the OneSignal **App ID** and **REST API Key** to `server/.env`:
+1. [OneSignal Dashboard](https://onesignal.com) でアプリを作成し、Firebase を設定
+2. Median.co で OneSignal を有効にし、App ID を入力
+3. OneSignal の **App ID** と **REST API Key** を `server/.env` に追加:
```env
ONESIGNAL_APP_ID=your_onesignal_app_id
ONESIGNAL_REST_KEY=your_onesignal_rest_api_key
```
-> **When not configured**: push notifications are silently disabled — all other features work normally.
+> **未設定時**:プッシュ通知は自動的に無効化されます — 他の機能には影響しません。
---
-## iOS — Permanent No-Cert Deployment
+## iOS — 証明書不要の永続デプロイ
-1. Deploy to a server with an HTTPS domain (required for WebRTC and Web Crypto APIs)
-2. Open `https://your.domain.com` in **Safari**
-3. Tap the Share button ⬆️ at the bottom of the screen
-4. Select **Add to Home Screen** → **Add**
+1. HTTPS ドメインのサーバーにデプロイ(WebRTC と Web Crypto API には HTTPS が必要)
+2. **Safari** で `https://your.domain.com` を開く
+3. 画面下部の共有ボタン ⬆️ をタップ
+4. **ホーム画面に追加** → **追加** を選択
-The app behaves identically to a native app — no Apple enterprise certificate required, no expiry.
+ネイティブアプリと同様に動作します — Apple エンタープライズ証明書不要、期限なし。
---
-## Production Deployment (Nginx)
+## 本番デプロイ(Nginx)
```nginx
server {
@@ -227,7 +228,7 @@ server {
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
- # Frontend static files
+ # フロントエンド静的ファイル
location / {
root /path/to/paperphone/client;
try_files $uri /index.html;
@@ -239,7 +240,7 @@ server {
proxy_set_header Host $host;
}
- # WebSocket (messaging + call signalling)
+ # WebSocket(メッセージング+通話シグナリング)
location /ws {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
@@ -252,125 +253,69 @@ server {
---
-## Project Structure
+## データベーススキーマ
+
+11テーブル、サーバー初回起動時に自動作成(`CREATE TABLE IF NOT EXISTS`):
+
+| テーブル | 用途 |
+|----------|------|
+| `users` | ユーザー情報 + ECDH/OPK 公開鍵 |
+| `prekeys` | X3DH ワンタイムプリキープール |
+| `friends` | 友達関係(pending / accepted / blocked) |
+| `groups` / `group_members` | グループチャット+メンバー(通知オフ状態含む) |
+| `messages` | 暗号化メッセージ(オフラインバッファ、配信後削除可能) |
+| `moments` | ソーシャル投稿(テキスト ≤ 1024文字) |
+| `moment_images` | 投稿画像(1投稿最大9枚) |
+| `moment_likes` | いいね(ユーザーごと投稿ごとにユニーク) |
+| `moment_comments` | コメント(≤ 512文字/件) |
+| `push_subscriptions` | Web Push サブスクリプション(VAPID) |
+| `onesignal_players` | OneSignal デバイス登録(Median.co) |
+
+---
+
+## セキュリティモデル
```
-paperphone/
-├── docker-compose.yml
-├── server/
-│ ├── .env # Environment variables (incl. Cloudflare TURN keys)
-│ └── src/
-│ ├── app.js # Express application
-│ ├── routes/
-│ │ ├── auth.js # Register / Login (incl. X3DH public key upload)
-│ │ ├── users.js # User search / Prekey bundle download
-│ │ ├── friends.js # Friend requests / Accept (incl. offline push)
-│ │ ├── groups.js # Group management
-│ │ ├── messages.js # Historical messages (paginated ciphertext)
-│ │ ├── upload.js # Cloudflare R2 file upload
-│ │ ├── files.js # File proxy (when R2_PUBLIC_URL is not set)
-│ │ ├── moments.js # Moments feed (posts / likes / comments)
-│ │ ├── calls.js # TURN credential issuance
-│ │ └── push.js # Push subscription mgmt (Web Push + OneSignal)
-│ ├── services/
-│ │ ├── push.js # Web Push VAPID service
-│ │ └── onesignal.js # OneSignal REST API service
-│ └── ws/
-│ └── wsServer.js # WebSocket router (messages + call signalling + offline push)
-│
-└── client/
- ├── index.html # SPA entry + PWA meta + Median push bridge
- ├── manifest.json # PWA manifest
- ├── sw.js # Service Worker (offline cache + push notifications)
- └── src/
- ├── style.css # Premium design system (dark/light, glassmorphism)
- ├── app.js # Router + global state + incoming call listener
- ├── api.js # HTTP client
- ├── socket.js # WebSocket client (auto-reconnect)
- ├── i18n.js # Multi-language engine (zh / en / ja / ko / fr / de / ru / es)
- ├── services/
- │ ├── webrtc.js # WebRTC manager — CallManager class
- │ └── pushNotification.js # Push subscription mgmt (Web Push + Median bridge)
- ├── crypto/
- │ ├── ratchet.js # X3DH + Double Ratchet + ML-KEM-768
- │ └── keystore.js # IndexedDB private key store
- ├── pages/
- │ ├── login.js # Login / Register (key generation, language picker)
- │ ├── chats.js # Chat list
- │ ├── chat.js # Chat window (E2E encryption, call buttons)
- │ ├── groups.js # Group list (create group, search)
- │ ├── groupInfo.js # Group info (member management, DND, leave/disband)
- │ ├── contacts.js # Contacts (friend requests, online status)
- │ ├── discover.js # Discover page
- │ ├── profile.js # Me / Settings (language, fingerprint, notifications, PWA)
- │ └── call.js # Call UI (incoming / active / multi-party video)
+登録時:
+ デバイスが IK(アイデンティティキー)+ SPK(署名付きプリキー)+ 10× OPK(ワンタイムプリキー)を生成
+ 公開鍵はアップロード、秘密鍵はデバイスに保存(4層永続化)
+
+メッセージ送信時:
+ 送信者が受信者の IK 公開鍵を取得
+ 一時 ECDH 鍵ペアを生成(メッセージごとに新しいペア)
+ X25519 ECDH → 共有秘密 → XSalsa20-Poly1305 暗号化
+ 一時公開鍵はメッセージヘッダーで送信、使用後に破棄
+
+サーバーが見るもの:
+ ✅ 暗号文ブロブ+ルーティングメタデータ(送受信者UUID、タイムスタンプ)
+ ❌ 平文 / 秘密鍵 / 一時鍵 / 通話内容
```
---
-## Database Schema
+## 環境変数リファレンス
-11 tables, auto-created on first server startup (`CREATE TABLE IF NOT EXISTS`):
-
-| Table | Purpose |
-|-------|---------|
-| `users` | User profiles + ECDH/OPK public keys |
-| `prekeys` | X3DH one-time prekey pool |
-| `friends` | Friendship relationships (pending / accepted / blocked) |
-| `groups` / `group_members` | Group chats + membership (incl. mute/DND status) |
-| `messages` | Encrypted payloads (offline buffer, deletable after delivery) |
-| `moments` | Social posts (text ≤ 1024 chars) |
-| `moment_images` | Post images (up to 9 per post) |
-| `moment_likes` | Likes (unique per user per post) |
-| `moment_comments` | Comments (≤ 512 chars each) |
-| `push_subscriptions` | Web Push subscriptions (VAPID) |
-| `onesignal_players` | OneSignal device registrations (Median.co) |
+| 変数 | 説明 | デフォルト |
+|------|------|------------|
+| `PORT` | サーバーポート | `3000` |
+| `JWT_SECRET` | JWT 署名キー(**本番環境では必ず変更**) | dev_secret |
+| `DB_HOST` / `DB_PASS` / `DB_NAME` | MySQL 接続設定 | — |
+| `REDIS_HOST` / `REDIS_PASS` | Redis 接続設定 | — |
+| `R2_ACCOUNT_ID` | Cloudflare アカウント ID | — |
+| `R2_ACCESS_KEY_ID` | R2 API トークンのアクセスキー | — |
+| `R2_SECRET_ACCESS_KEY` | R2 API トークンのシークレットキー | — |
+| `R2_BUCKET` | R2 バケット名 | — |
+| `R2_PUBLIC_URL` | R2 公開 URL(任意)— CDN 直リンクを有効化 | — |
+| `CF_CALLS_APP_ID` | Cloudflare Calls App ID(任意) | — |
+| `CF_CALLS_APP_SECRET` | Cloudflare Calls App Secret(任意) | — |
+| `VAPID_PUBLIC_KEY` | Web Push VAPID 公開鍵(任意) | — |
+| `VAPID_PRIVATE_KEY` | Web Push VAPID 秘密鍵(任意) | — |
+| `VAPID_SUBJECT` | VAPID 連絡先メール(任意) | `mailto:admin@paperphone.app` |
+| `ONESIGNAL_APP_ID` | OneSignal App ID(任意、Median.co用) | — |
+| `ONESIGNAL_REST_KEY` | OneSignal REST API Key(任意) | — |
---
-## Security Model
-
-```
-On Registration:
- Device generates IK (Identity Key) + SPK (Signed PreKey) + 10× OPK (One-Time PreKeys)
- Public keys are uploaded; private keys stay on-device (four-tier persistence)
-
-On Each Message:
- Sender fetches recipient's IK public key
- Generates a fresh ephemeral ECDH keypair (per-message)
- X25519 ECDH → shared secret → XSalsa20-Poly1305 encrypt
- Ephemeral public key sent in message header; destroyed after use
-
-What the Server Sees:
- ✅ Ciphertext blob + routing metadata (sender / recipient UUID, timestamps)
- ❌ Plaintext / private keys / ephemeral keys / call content
-```
-
----
-
-## Environment Variables
-
-| Variable | Description | Default |
-|----------|-------------|---------|
-| `PORT` | Server port | `3000` |
-| `JWT_SECRET` | JWT signing key (**change in production**) | dev_secret |
-| `DB_HOST` / `DB_PASS` / `DB_NAME` | MySQL connection | — |
-| `REDIS_HOST` / `REDIS_PASS` | Redis connection | — |
-| `R2_ACCOUNT_ID` | Cloudflare account ID | — |
-| `R2_ACCESS_KEY_ID` | R2 API token access key | — |
-| `R2_SECRET_ACCESS_KEY` | R2 API token secret key | — |
-| `R2_BUCKET` | R2 bucket name | — |
-| `R2_PUBLIC_URL` | R2 public base URL (optional) — enables direct CDN links | — |
-| `CF_CALLS_APP_ID` | Cloudflare Calls App ID (optional) | — |
-| `CF_CALLS_APP_SECRET` | Cloudflare Calls App Secret (optional) | — |
-| `VAPID_PUBLIC_KEY` | Web Push VAPID public key (optional) | — |
-| `VAPID_PRIVATE_KEY` | Web Push VAPID private key (optional) | — |
-| `VAPID_SUBJECT` | VAPID contact email (optional) | `mailto:admin@paperphone.app` |
-| `ONESIGNAL_APP_ID` | OneSignal App ID (optional, for Median.co) | — |
-| `ONESIGNAL_REST_KEY` | OneSignal REST API Key (optional) | — |
-
----
-
-## License
+## ライセンス
MIT © PaperPhone Contributors
diff --git a/README_KO.md b/README_KO.md
index 5a182dc..d8d55eb 100644
--- a/README_KO.md
+++ b/README_KO.md
@@ -10,112 +10,113 @@ WeChat 스타일의 종단간 암호화 인스턴트 메시징 앱. 무상태 EC
---
-## Features
-| Feature | Description |
-|---------|-------------|
-| 🔐 End-to-End Encryption | Stateless ECDH + XSalsa20-Poly1305 — ephemeral keys per message, forward secrecy |
-| 🗑️ Zero-Knowledge Server | Server stores only ciphertext; private keys never leave the device |
-| 📹 Video & Voice Calls | WebRTC P2P (1:1) + Mesh (group), Cloudflare TURN for NAT traversal |
-| 👥 Group Chat | Up to 2000 members, plain-text messages (no encryption), Do Not Disturb mode, member management |
-| ⏱️ Auto-Delete Messages | 5 tiers (never / 1 day / 3 days / 1 week / 1 month), settable by either party in DMs, owner-only in groups |
-| 🔔 Push Notifications | Web Push (VAPID) + OneSignal dual-channel — reach users even when offline |
-| 🌐 Multi-Language | Chinese, English, Japanese, Korean, French, German, Russian, Spanish — auto-detect + manual switch |
-| 📱 iOS — No Enterprise Cert | PWA via Safari "Add to Home Screen", works permanently without Apple signing |
-| 💬 Rich Messaging | Text, images, voice messages, 64-emoji panel, delivery receipts, typing indicators |
-| 🌐 Moments | WeChat-style social feed: text + up to 9 photos, likes (friend avatars), comments, tag-based visibility control |
-| 🏷️ Friend Tags | Assign multiple tags to friends (12-color preset palette), filter contacts by tag |
-| 🗂️ R2 Object Storage | Cloudflare R2 for image/voice files — optional public CDN URL |
-| 🏗️ Self-Hostable | Docker Compose one-command deployment; Node.js + Redis multi-node ready |
+## 기능
+
+| 기능 | 설명 |
+|------|------|
+| 🔐 종단간 암호화 | 무상태 ECDH + XSalsa20-Poly1305 — 메시지별 임시 키, 전방 비밀성 |
+| 🗝️ 제로 지식 서버 | 서버는 암호문만 저장, 개인 키는 기기를 떠나지 않음 |
+| 📹 영상/음성 통화 | WebRTC P2P (1:1) + Mesh (그룹), Cloudflare TURN을 통한 NAT 트래버설 |
+| 👥 그룹 채팅 | 최대 2000명, 일반 텍스트 메시지 (비암호화), 방해 금지 모드, 멤버 관리 |
+| ⏱️ 메시지 자동 삭제 | 5단계 (안함/1일/3일/1주/1개월), DM에서 양쪽 설정 가능, 그룹은 방장만 |
+| 🔔 알림 | Web Push (VAPID) + OneSignal 이중 채널 — 오프라인에서도 알림 수신 |
+| 🌐 다국어 | 중국어·영어·일본어·한국어·프랑스어·독일어·러시아어·스페인어 (자동 감지 + 수동 전환) |
+| 📱 iOS — 기업 인증서 불필요 | Safari "홈 화면에 추가"를 통한 PWA, Apple 서명 없이 영구 작동 |
+| 💬 풍부한 메시징 | 텍스트, 이미지, 음성 메시지, 이모지 패널 (64종), 읽음 확인 |
+| 🌐 모먼트 | 텍스트 + 최대 9장 사진, 좋아요 (친구 아바타 표시), 댓글, 태그 기반 공개 범위 제어 |
+| 🏷️ 친구 태그 | 친구에게 여러 태그 할당 (12색 프리셋), 태그별 연락처 필터링 |
+| 🗂️ R2 오브젝트 스토리지 | Cloudflare R2로 이미지/음성 파일 저장 — 선택적 공개 CDN URL |
+| 🏗️ 셀프 호스팅 가능 | Docker Compose 원커맨드 배포, Node.js + Redis 멀티 노드 지원 |
---
-## Tech Stack
+## 기술 스택
```
-Backend (server/)
+백엔드 (server/)
Node.js 20 + Express + ws
- MySQL 8.0 — users, messages (persisted ciphertext)
- Redis — online presence + cross-node routing
- Cloudflare R2 — image/voice file storage (S3-compatible API)
- JWT + bcrypt authentication
+ MySQL 8.0 — 사용자, 메시지 영속화 (암호문)
+ Redis — 온라인 상태 + 크로스 노드 라우팅
+ Cloudflare R2 — 이미지/음성 파일 저장소 (S3 호환 API)
+ JWT + bcrypt 인증
-Frontend (client/)
- Native HTML + Vanilla JS (ESM, no bundler required)
+프론트엔드 (client/)
+ 네이티브 HTML + Vanilla JS (ESM, 번들러 불필요)
libsodium-wrappers (WebAssembly — Curve25519 / XSalsa20-Poly1305)
- WebRTC API — video / voice calls
+ WebRTC API — 영상/음성 통화
PWA: manifest.json + Service Worker
-Cryptographic Layer
- Stateless ECDH + XSalsa20-Poly1305 — ephemeral keypair per message
- Four-tier key persistence: memory → localStorage → sessionStorage → IndexedDB
- All private keys stored on-device only — never sent to the server
+암호화 레이어
+ 무상태 ECDH + XSalsa20-Poly1305 — 메시지별 임시 ECDH 키페어
+ 개인 키 4단계 영속화: 메모리 → localStorage → sessionStorage → IndexedDB
+ 모든 개인 키는 기기에만 저장 — 절대 서버로 전송되지 않음
```
---
-## Quick Start
+## 빠른 시작
-### Option 0: Zeabur One-Click Cloud Deploy
+### 방법 0: Zeabur 원클릭 클라우드 배포
[](https://zeabur.com/templates/P2J7Y3?referralCode=619dev)
> [!NOTE]
-> One manual step is required after the template deploys, otherwise login/register won't work:
-> 1. Go to Zeabur Console → **server service** → Environment Variables → copy the value of `ZEABUR_WEB_URL` (e.g. `http://10.43.x.x:3000`)
-> 2. Go to **client service** → Environment Variables → add variable `SERVER_URL` = the value copied above
-> 3. Restart the client service
+> 템플릿 배포 후 수동 단계 하나가 필요합니다. 이 작업을 하지 않으면 로그인/회원가입이 작동하지 않습니다:
+> 1. Zeabur 콘솔 → **server 서비스** → 환경 변수 → `ZEABUR_WEB_URL` 값 복사 (예: `http://10.43.x.x:3000`)
+> 2. **client 서비스** → 환경 변수 → 변수 `SERVER_URL` 추가 = 위에서 복사한 값
+> 3. client 서비스 재시작
-**Known notes:**
-- On first startup, the server automatically creates all database tables (`CREATE TABLE IF NOT EXISTS`) — no manual SQL import needed
-- Redis runs without a password inside the cluster (intra-cluster network isolation is sufficient)
-- If MySQL access is denied, manually set `DB_PASS` on the server service to the value of `MYSQL_ROOT_PASSWORD` from the MySQL service
-- To get the **internal IP** of any service container, open that service's Terminal in the Zeabur console and run:
+**알려진 참고 사항:**
+- 첫 시작 시 서버가 자동으로 모든 데이터베이스 테이블을 생성합니다 (`CREATE TABLE IF NOT EXISTS`) — SQL 수동 임포트 불필요
+- Redis는 클러스터 내에서 비밀번호 없이 작동합니다
+- MySQL 접근이 거부되면 server 서비스의 `DB_PASS`를 MySQL 서비스의 `MYSQL_ROOT_PASSWORD` 값으로 수동 설정하세요
+- 서비스 컨테이너의 **내부 IP**를 확인하려면 Zeabur 콘솔에서 해당 서비스의 터미널을 열고 실행:
```bash
hostname -i
```
---
-### Option 1: Docker Compose (Recommended — no local build needed)
+### 방법 1: Docker Compose (권장 — 로컬 빌드 불필요)
```bash
-# Clone the repository
+# 저장소 클론
git clone && cd paperphone
-# Copy and edit environment variables
+# 환경 변수 복사 및 편집
cp server/.env.example server/.env
-# Fill in: DB_PASS / JWT_SECRET / CF_CALLS_APP_ID etc.
+# DB_PASS / JWT_SECRET / R2_* 등 입력
-# Pull images and start everything
+# 이미지 풀 및 시작
docker compose up -d
-# Check service status
+# 서비스 상태 확인
docker compose ps
-# Open in browser
+# 브라우저에서 열기
open http://localhost
```
-> Pre-built images on Docker Hub:
+> Docker Hub의 사전 빌드 이미지:
> - `facilisvelox/paperphone-client:latest`
> - `facilisvelox/paperphone-server:latest`
>
-> **Note**: The server automatically initialises the database schema on first startup — no manual SQL import required.
+> **참고**: 서버는 첫 시작 시 자동으로 데이터베이스 스키마를 초기화합니다 — SQL 수동 임포트가 필요하지 않습니다.
-### Option 2: Manual Local Start
+### 방법 2: 로컬 수동 시작
-#### 1. Prepare the environment
+#### 1. 환경 준비
```bash
-# Copy and edit environment variables
+# 환경 변수 복사 및 편집
cp server/.env.example server/.env
-# Fill in DB_HOST / DB_PASS / REDIS_HOST / R2_* etc.
+# DB_HOST / DB_PASS / REDIS_HOST / R2_* 등 입력
-# Note: the server auto-runs schema.sql on first startup
+# 참고: 서버는 첫 시작 시 자동으로 schema.sql을 실행합니다
```
-#### 2. Start the backend
+#### 2. 백엔드 시작
```bash
cd server
@@ -123,7 +124,7 @@ npm install
npm run dev # → http://localhost:3000
```
-#### 3. Start the frontend
+#### 3. 프론트엔드 시작
```bash
npx serve client -p 8080
@@ -132,54 +133,54 @@ npx serve client -p 8080
---
-## Video Call Configuration
+## 영상 통화 설정
-Video and voice calls use WebRTC P2P and work out of the box on the same LAN. For cross-network calls, a TURN server is required for NAT traversal.
+영상 및 음성 통화는 WebRTC P2P를 사용하며 동일 LAN에서 바로 사용할 수 있습니다. 다른 네트워크 간 통화에는 NAT 트래버설을 위한 TURN 서버가 필요합니다.
-### Using Cloudflare TURN (Recommended)
+### Cloudflare TURN 사용 (권장)
-1. Log in to [Cloudflare Dashboard](https://dash.cloudflare.com) → **Workers & Pages** → **Calls** → create an App
-2. Copy the **App ID** and **App Secret** (Token Key)
-3. Add them to `server/.env`:
+1. [Cloudflare Dashboard](https://dash.cloudflare.com) → **Workers & Pages** → **Calls** → 앱 생성
+2. **App ID**와 **App Secret** (토큰 키) 복사
+3. `server/.env`에 추가:
```env
CF_CALLS_APP_ID=your_app_id_here
CF_CALLS_APP_SECRET=your_app_secret_here
```
-4. Restart the backend — TURN credentials are fetched automatically per call session (TTL: 86 400 s)
+4. 백엔드 재시작 — TURN 자격 증명은 통화 세션마다 자동 발급됩니다 (TTL: 86,400초)
-> **Without credentials**: the server falls back to STUN only (Google + Cloudflare public STUN). Calls work on LAN without any extra configuration.
+> **미설정 시**: STUN 전용으로 폴백합니다 (Google + Cloudflare 공개 STUN). LAN 통화는 추가 설정 없이 작동합니다.
-### Call Types
+### 통화 유형
-| Type | Transport | Recommended for |
-|------|-----------|-----------------|
-| 1:1 Video Call | WebRTC P2P + TURN | All scenarios |
-| 1:1 Voice Call | WebRTC P2P + TURN | All scenarios |
-| Group Video / Voice | WebRTC Mesh (full-mesh) | Up to 6 participants |
+| 유형 | 전송 방식 | 권장 사용 |
+|------|-----------|-----------|
+| 1:1 영상 통화 | WebRTC P2P + TURN | 모든 시나리오 |
+| 1:1 음성 통화 | WebRTC P2P + TURN | 모든 시나리오 |
+| 그룹 영상/음성 | WebRTC Mesh (풀 메시) | 최대 6명 |
---
-## Push Notification Configuration
+## 푸시 알림 설정
-Offline message notifications are delivered through **two channels** for maximum delivery rate:
+오프라인 메시지 알림은 **두 채널**을 통해 배달되어 최대 전달률을 보장합니다:
-| Channel | Platforms | Configuration |
-|---------|-----------|---------------|
-| Web Push (VAPID) | Browsers (Chrome/Edge/Firefox) + iOS PWA (Safari 16.4+) | VAPID keys |
-| OneSignal | Native Android/iOS apps via Median.co | OneSignal App ID + REST Key |
+| 채널 | 플랫폼 | 설정 |
+|------|--------|------|
+| Web Push (VAPID) | 브라우저 (Chrome/Edge/Firefox) + iOS PWA (Safari 16.4+) | VAPID 키 |
+| OneSignal | Median.co 경유 네이티브 Android/iOS 앱 | OneSignal App ID + REST Key |
-### Configuring Web Push
+### Web Push 설정
-1. Generate VAPID keys (one-time):
+1. VAPID 키 생성 (1회만):
```bash
cd server
npx web-push generate-vapid-keys
```
-2. Add to `server/.env`:
+2. `server/.env`에 추가:
```env
VAPID_PUBLIC_KEY=your_public_key_here
@@ -187,37 +188,37 @@ VAPID_PRIVATE_KEY=your_private_key_here
VAPID_SUBJECT=mailto:admin@your-domain.com
```
-3. Restart the server — users can enable notifications from the Settings page
+3. 서버 재시작 — 사용자가 설정 페이지에서 알림을 활성화할 수 있습니다
-> **iOS users** must first "Add to Home Screen" (PWA), and only iOS 16.4+ is supported.
+> **iOS 사용자**는 먼저 "홈 화면에 추가" (PWA)를 해야 하며, iOS 16.4 이상만 지원됩니다.
-### Configuring OneSignal (Median.co Native Apps)
+### OneSignal 설정 (Median.co 네이티브 앱)
-1. Create an app on [OneSignal Dashboard](https://onesignal.com) and configure Firebase
-2. Enable OneSignal in Median.co and enter the App ID
-3. Add the OneSignal **App ID** and **REST API Key** to `server/.env`:
+1. [OneSignal Dashboard](https://onesignal.com)에서 앱 생성 및 Firebase 설정
+2. Median.co에서 OneSignal 활성화 후 App ID 입력
+3. OneSignal **App ID**와 **REST API Key**를 `server/.env`에 추가:
```env
ONESIGNAL_APP_ID=your_onesignal_app_id
ONESIGNAL_REST_KEY=your_onesignal_rest_api_key
```
-> **When not configured**: push notifications are silently disabled — all other features work normally.
+> **미설정 시**: 푸시 알림이 자동으로 비활성화됩니다 — 다른 기능에는 영향 없습니다.
---
-## iOS — Permanent No-Cert Deployment
+## iOS — 인증서 없는 영구 배포
-1. Deploy to a server with an HTTPS domain (required for WebRTC and Web Crypto APIs)
-2. Open `https://your.domain.com` in **Safari**
-3. Tap the Share button ⬆️ at the bottom of the screen
-4. Select **Add to Home Screen** → **Add**
+1. HTTPS 도메인 서버에 배포 (WebRTC 및 Web Crypto API에 HTTPS 필요)
+2. **Safari**에서 `https://your.domain.com` 열기
+3. 화면 하단의 공유 버튼 ⬆️ 탭
+4. **홈 화면에 추가** → **추가** 선택
-The app behaves identically to a native app — no Apple enterprise certificate required, no expiry.
+네이티브 앱과 동일하게 작동합니다 — Apple 기업 인증서 불필요, 만료 없음.
---
-## Production Deployment (Nginx)
+## 프로덕션 배포 (Nginx)
```nginx
server {
@@ -227,7 +228,7 @@ server {
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
- # Frontend static files
+ # 프론트엔드 정적 파일
location / {
root /path/to/paperphone/client;
try_files $uri /index.html;
@@ -239,7 +240,7 @@ server {
proxy_set_header Host $host;
}
- # WebSocket (messaging + call signalling)
+ # WebSocket (메시징 + 통화 시그널링)
location /ws {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
@@ -252,125 +253,69 @@ server {
---
-## Project Structure
+## 데이터베이스 스키마
+
+11개 테이블, 서버 첫 시작 시 자동 생성 (`CREATE TABLE IF NOT EXISTS`):
+
+| 테이블 | 용도 |
+|--------|------|
+| `users` | 사용자 프로필 + ECDH/OPK 공개 키 |
+| `prekeys` | X3DH 원타임 프리키 풀 |
+| `friends` | 친구 관계 (pending / accepted / blocked) |
+| `groups` / `group_members` | 그룹 채팅 + 멤버 (알림 끄기 상태 포함) |
+| `messages` | 암호화된 메시지 (오프라인 버퍼, 전달 후 삭제 가능) |
+| `moments` | 소셜 게시물 (텍스트 ≤ 1024자) |
+| `moment_images` | 게시물 이미지 (게시물당 최대 9개) |
+| `moment_likes` | 좋아요 (사용자당 게시물당 고유) |
+| `moment_comments` | 댓글 (≤ 512자) |
+| `push_subscriptions` | Web Push 구독 (VAPID) |
+| `onesignal_players` | OneSignal 기기 등록 (Median.co) |
+
+---
+
+## 보안 모델
```
-paperphone/
-├── docker-compose.yml
-├── server/
-│ ├── .env # Environment variables (incl. Cloudflare TURN keys)
-│ └── src/
-│ ├── app.js # Express application
-│ ├── routes/
-│ │ ├── auth.js # Register / Login (incl. X3DH public key upload)
-│ │ ├── users.js # User search / Prekey bundle download
-│ │ ├── friends.js # Friend requests / Accept (incl. offline push)
-│ │ ├── groups.js # Group management
-│ │ ├── messages.js # Historical messages (paginated ciphertext)
-│ │ ├── upload.js # Cloudflare R2 file upload
-│ │ ├── files.js # File proxy (when R2_PUBLIC_URL is not set)
-│ │ ├── moments.js # Moments feed (posts / likes / comments)
-│ │ ├── calls.js # TURN credential issuance
-│ │ └── push.js # Push subscription mgmt (Web Push + OneSignal)
-│ ├── services/
-│ │ ├── push.js # Web Push VAPID service
-│ │ └── onesignal.js # OneSignal REST API service
-│ └── ws/
-│ └── wsServer.js # WebSocket router (messages + call signalling + offline push)
-│
-└── client/
- ├── index.html # SPA entry + PWA meta + Median push bridge
- ├── manifest.json # PWA manifest
- ├── sw.js # Service Worker (offline cache + push notifications)
- └── src/
- ├── style.css # Premium design system (dark/light, glassmorphism)
- ├── app.js # Router + global state + incoming call listener
- ├── api.js # HTTP client
- ├── socket.js # WebSocket client (auto-reconnect)
- ├── i18n.js # Multi-language engine (zh / en / ja / ko / fr / de / ru / es)
- ├── services/
- │ ├── webrtc.js # WebRTC manager — CallManager class
- │ └── pushNotification.js # Push subscription mgmt (Web Push + Median bridge)
- ├── crypto/
- │ ├── ratchet.js # X3DH + Double Ratchet + ML-KEM-768
- │ └── keystore.js # IndexedDB private key store
- ├── pages/
- │ ├── login.js # Login / Register (key generation, language picker)
- │ ├── chats.js # Chat list
- │ ├── chat.js # Chat window (E2E encryption, call buttons)
- │ ├── groups.js # Group list (create group, search)
- │ ├── groupInfo.js # Group info (member management, DND, leave/disband)
- │ ├── contacts.js # Contacts (friend requests, online status)
- │ ├── discover.js # Discover page
- │ ├── profile.js # Me / Settings (language, fingerprint, notifications, PWA)
- │ └── call.js # Call UI (incoming / active / multi-party video)
+등록 시:
+ 기기가 IK (아이덴티티 키) + SPK (서명 프리키) + 10× OPK (원타임 프리키) 생성
+ 공개 키는 업로드, 개인 키는 기기에 보관 (4단계 영속화)
+
+메시지 전송 시:
+ 발신자가 수신자의 IK 공개 키를 가져옴
+ 임시 ECDH 키페어 생성 (메시지마다 새로운 페어)
+ X25519 ECDH → 공유 비밀 → XSalsa20-Poly1305 암호화
+ 임시 공개 키는 메시지 헤더로 전송, 사용 후 파기
+
+서버가 보는 것:
+ ✅ 암호문 블롭 + 라우팅 메타데이터 (발신자/수신자 UUID, 타임스탬프)
+ ❌ 평문 / 개인 키 / 임시 키 / 통화 내용
```
---
-## Database Schema
+## 환경 변수
-11 tables, auto-created on first server startup (`CREATE TABLE IF NOT EXISTS`):
-
-| Table | Purpose |
-|-------|---------|
-| `users` | User profiles + ECDH/OPK public keys |
-| `prekeys` | X3DH one-time prekey pool |
-| `friends` | Friendship relationships (pending / accepted / blocked) |
-| `groups` / `group_members` | Group chats + membership (incl. mute/DND status) |
-| `messages` | Encrypted payloads (offline buffer, deletable after delivery) |
-| `moments` | Social posts (text ≤ 1024 chars) |
-| `moment_images` | Post images (up to 9 per post) |
-| `moment_likes` | Likes (unique per user per post) |
-| `moment_comments` | Comments (≤ 512 chars each) |
-| `push_subscriptions` | Web Push subscriptions (VAPID) |
-| `onesignal_players` | OneSignal device registrations (Median.co) |
+| 변수 | 설명 | 기본값 |
+|------|------|--------|
+| `PORT` | 서버 포트 | `3000` |
+| `JWT_SECRET` | JWT 서명 키 (**프로덕션에서 반드시 변경**) | dev_secret |
+| `DB_HOST` / `DB_PASS` / `DB_NAME` | MySQL 연결 | — |
+| `REDIS_HOST` / `REDIS_PASS` | Redis 연결 | — |
+| `R2_ACCOUNT_ID` | Cloudflare 계정 ID | — |
+| `R2_ACCESS_KEY_ID` | R2 API 토큰 액세스 키 | — |
+| `R2_SECRET_ACCESS_KEY` | R2 API 토큰 시크릿 키 | — |
+| `R2_BUCKET` | R2 버킷 이름 | — |
+| `R2_PUBLIC_URL` | R2 공개 기본 URL (선택) — CDN 직접 링크 활성화 | — |
+| `CF_CALLS_APP_ID` | Cloudflare Calls App ID (선택) | — |
+| `CF_CALLS_APP_SECRET` | Cloudflare Calls App Secret (선택) | — |
+| `VAPID_PUBLIC_KEY` | Web Push VAPID 공개 키 (선택) | — |
+| `VAPID_PRIVATE_KEY` | Web Push VAPID 개인 키 (선택) | — |
+| `VAPID_SUBJECT` | VAPID 연락처 이메일 (선택) | `mailto:admin@paperphone.app` |
+| `ONESIGNAL_APP_ID` | OneSignal App ID (선택, Median.co용) | — |
+| `ONESIGNAL_REST_KEY` | OneSignal REST API Key (선택) | — |
---
-## Security Model
-
-```
-On Registration:
- Device generates IK (Identity Key) + SPK (Signed PreKey) + 10× OPK (One-Time PreKeys)
- Public keys are uploaded; private keys stay on-device (four-tier persistence)
-
-On Each Message:
- Sender fetches recipient's IK public key
- Generates a fresh ephemeral ECDH keypair (per-message)
- X25519 ECDH → shared secret → XSalsa20-Poly1305 encrypt
- Ephemeral public key sent in message header; destroyed after use
-
-What the Server Sees:
- ✅ Ciphertext blob + routing metadata (sender / recipient UUID, timestamps)
- ❌ Plaintext / private keys / ephemeral keys / call content
-```
-
----
-
-## Environment Variables
-
-| Variable | Description | Default |
-|----------|-------------|---------|
-| `PORT` | Server port | `3000` |
-| `JWT_SECRET` | JWT signing key (**change in production**) | dev_secret |
-| `DB_HOST` / `DB_PASS` / `DB_NAME` | MySQL connection | — |
-| `REDIS_HOST` / `REDIS_PASS` | Redis connection | — |
-| `R2_ACCOUNT_ID` | Cloudflare account ID | — |
-| `R2_ACCESS_KEY_ID` | R2 API token access key | — |
-| `R2_SECRET_ACCESS_KEY` | R2 API token secret key | — |
-| `R2_BUCKET` | R2 bucket name | — |
-| `R2_PUBLIC_URL` | R2 public base URL (optional) — enables direct CDN links | — |
-| `CF_CALLS_APP_ID` | Cloudflare Calls App ID (optional) | — |
-| `CF_CALLS_APP_SECRET` | Cloudflare Calls App Secret (optional) | — |
-| `VAPID_PUBLIC_KEY` | Web Push VAPID public key (optional) | — |
-| `VAPID_PRIVATE_KEY` | Web Push VAPID private key (optional) | — |
-| `VAPID_SUBJECT` | VAPID contact email (optional) | `mailto:admin@paperphone.app` |
-| `ONESIGNAL_APP_ID` | OneSignal App ID (optional, for Median.co) | — |
-| `ONESIGNAL_REST_KEY` | OneSignal REST API Key (optional) | — |
-
----
-
-## License
+## 라이선스
MIT © PaperPhone Contributors
diff --git a/README_RU.md b/README_RU.md
index 0e6026b..c2254fd 100644
--- a/README_RU.md
+++ b/README_RU.md
@@ -10,367 +10,173 @@
---
-## Features
-| Feature | Description |
-|---------|-------------|
-| 🔐 End-to-End Encryption | Stateless ECDH + XSalsa20-Poly1305 — ephemeral keys per message, forward secrecy |
-| 🗑️ Zero-Knowledge Server | Server stores only ciphertext; private keys never leave the device |
-| 📹 Video & Voice Calls | WebRTC P2P (1:1) + Mesh (group), Cloudflare TURN for NAT traversal |
-| 👥 Group Chat | Up to 2000 members, plain-text messages (no encryption), Do Not Disturb mode, member management |
-| ⏱️ Auto-Delete Messages | 5 tiers (never / 1 day / 3 days / 1 week / 1 month), settable by either party in DMs, owner-only in groups |
-| 🔔 Push Notifications | Web Push (VAPID) + OneSignal dual-channel — reach users even when offline |
-| 🌐 Multi-Language | Chinese, English, Japanese, Korean, French, German, Russian, Spanish — auto-detect + manual switch |
-| 📱 iOS — No Enterprise Cert | PWA via Safari "Add to Home Screen", works permanently without Apple signing |
-| 💬 Rich Messaging | Text, images, voice messages, 64-emoji panel, delivery receipts, typing indicators |
-| 🌐 Moments | WeChat-style social feed: text + up to 9 photos, likes (friend avatars), comments, tag-based visibility control |
-| 🏷️ Friend Tags | Assign multiple tags to friends (12-color preset palette), filter contacts by tag |
-| 🗂️ R2 Object Storage | Cloudflare R2 for image/voice files — optional public CDN URL |
-| 🏗️ Self-Hostable | Docker Compose one-command deployment; Node.js + Redis multi-node ready |
+## Возможности
+
+| Функция | Описание |
+|---------|----------|
+| 🔐 Сквозное шифрование | Безсостоянийный ECDH + XSalsa20-Poly1305 — эфемерные ключи на каждое сообщение, прямая секретность |
+| 🗝️ Сервер с нулевым знанием | Сервер хранит только шифротекст; закрытые ключи никогда не покидают устройство |
+| 📹 Видео/аудио звонки | WebRTC P2P (1:1) + Mesh (группа), Cloudflare TURN для NAT-обхода |
+| 👥 Групповой чат | До 2000 участников, обычный текст (без шифрования), режим «Не беспокоить», управление участниками |
+| ⏱️ Автоудаление | 5 уровней (никогда/1д/3д/1нед/1мес), в ЛС — обе стороны, в группах — только владелец |
+| 🔔 Push-уведомления | Web Push (VAPID) + OneSignal — двойной канал |
+| 🌐 Многоязычность | ZH/EN/JA/KO/FR/DE/RU/ES — автоопределение + ручной выбор |
+| 📱 iOS без сертификата | PWA через Safari «На экран Домой», без корпоративного сертификата Apple |
+| 💬 Расширенные сообщения | Текст, изображения, голосовые сообщения, 64 эмодзи, подтверждения прочтения |
+| 🌐 Моменты | Социальная лента: текст + до 9 фото, лайки (аватары друзей), комментарии, управление видимостью по тегам |
+| 🏷️ Теги друзей | Несколько тегов на друга (палитра из 12 цветов), фильтрация контактов по тегам |
+| 🗂️ Хранилище R2 | Cloudflare R2 для изображений/аудио — опциональный CDN URL |
+| 🏗️ Самостоятельное размещение | Docker Compose в одну команду |
---
-## Tech Stack
+## Технологический стек
```
-Backend (server/)
+Бэкенд (server/)
Node.js 20 + Express + ws
- MySQL 8.0 — users, messages (persisted ciphertext)
- Redis — online presence + cross-node routing
- Cloudflare R2 — image/voice file storage (S3-compatible API)
- JWT + bcrypt authentication
+ MySQL 8.0 — пользователи, сообщения (шифротекст)
+ Redis — статус онлайн + маршрутизация между узлами
+ Cloudflare R2 — хранилище файлов (S3-совместимый API)
+ JWT + bcrypt аутентификация
-Frontend (client/)
- Native HTML + Vanilla JS (ESM, no bundler required)
+Фронтенд (client/)
+ Нативный HTML + Vanilla JS (ESM, без сборщика)
libsodium-wrappers (WebAssembly — Curve25519 / XSalsa20-Poly1305)
- WebRTC API — video / voice calls
+ WebRTC API — видео/аудио звонки
PWA: manifest.json + Service Worker
-Cryptographic Layer
- Stateless ECDH + XSalsa20-Poly1305 — ephemeral keypair per message
- Four-tier key persistence: memory → localStorage → sessionStorage → IndexedDB
- All private keys stored on-device only — never sent to the server
+Криптографический слой
+ Безсостоянийный ECDH + XSalsa20-Poly1305 — эфемерная пара ключей на сообщение
+ 4-уровневое хранение ключей: память → localStorage → sessionStorage → IndexedDB
+ Закрытые ключи только на устройстве — никогда не отправляются на сервер
```
---
-## Quick Start
+## Быстрый старт
-### Option 0: Zeabur One-Click Cloud Deploy
+### Вариант 0: Zeabur — облачное развёртывание в один клик
[](https://zeabur.com/templates/P2J7Y3?referralCode=619dev)
> [!NOTE]
-> One manual step is required after the template deploys, otherwise login/register won't work:
-> 1. Go to Zeabur Console → **server service** → Environment Variables → copy the value of `ZEABUR_WEB_URL` (e.g. `http://10.43.x.x:3000`)
-> 2. Go to **client service** → Environment Variables → add variable `SERVER_URL` = the value copied above
-> 3. Restart the client service
+> После развёртывания шаблона необходим один ручной шаг:
+> 1. Консоль Zeabur → сервис **server** → Переменные → скопировать `ZEABUR_WEB_URL`
+> 2. Сервис **client** → Переменные → добавить `SERVER_URL` = скопированное значение
+> 3. Перезапустить сервис client
-**Known notes:**
-- On first startup, the server automatically creates all database tables (`CREATE TABLE IF NOT EXISTS`) — no manual SQL import needed
-- Redis runs without a password inside the cluster (intra-cluster network isolation is sufficient)
-- If MySQL access is denied, manually set `DB_PASS` on the server service to the value of `MYSQL_ROOT_PASSWORD` from the MySQL service
-- To get the **internal IP** of any service container, open that service's Terminal in the Zeabur console and run:
- ```bash
- hostname -i
- ```
-
----
-
-### Option 1: Docker Compose (Recommended — no local build needed)
+### Вариант 1: Docker Compose (рекомендуется)
```bash
-# Clone the repository
git clone && cd paperphone
-
-# Copy and edit environment variables
cp server/.env.example server/.env
-# Fill in: DB_PASS / JWT_SECRET / CF_CALLS_APP_ID etc.
-
-# Pull images and start everything
+# Заполнить: DB_PASS / JWT_SECRET / R2_* и т.д.
docker compose up -d
-
-# Check service status
-docker compose ps
-
-# Open in browser
open http://localhost
```
-> Pre-built images on Docker Hub:
-> - `facilisvelox/paperphone-client:latest`
-> - `facilisvelox/paperphone-server:latest`
->
-> **Note**: The server automatically initialises the database schema on first startup — no manual SQL import required.
+> Образы Docker Hub: `facilisvelox/paperphone-client:latest` и `facilisvelox/paperphone-server:latest`
-### Option 2: Manual Local Start
-
-#### 1. Prepare the environment
+### Вариант 2: Локальный ручной запуск
```bash
-# Copy and edit environment variables
-cp server/.env.example server/.env
-# Fill in DB_HOST / DB_PASS / REDIS_HOST / R2_* etc.
+# Бэкенд
+cd server && npm install && npm run dev # → http://localhost:3000
-# Note: the server auto-runs schema.sql on first startup
-```
-
-#### 2. Start the backend
-
-```bash
-cd server
-npm install
-npm run dev # → http://localhost:3000
-```
-
-#### 3. Start the frontend
-
-```bash
-npx serve client -p 8080
-# → http://localhost:8080
+# Фронтенд
+npx serve client -p 8080 # → http://localhost:8080
```
---
-## Video Call Configuration
-
-Video and voice calls use WebRTC P2P and work out of the box on the same LAN. For cross-network calls, a TURN server is required for NAT traversal.
-
-### Using Cloudflare TURN (Recommended)
-
-1. Log in to [Cloudflare Dashboard](https://dash.cloudflare.com) → **Workers & Pages** → **Calls** → create an App
-2. Copy the **App ID** and **App Secret** (Token Key)
-3. Add them to `server/.env`:
+## Настройка видеозвонков — Cloudflare TURN
```env
CF_CALLS_APP_ID=your_app_id_here
CF_CALLS_APP_SECRET=your_app_secret_here
```
-4. Restart the backend — TURN credentials are fetched automatically per call session (TTL: 86 400 s)
+| Тип | Транспорт | Рекомендуется для |
+|-----|-----------|-------------------|
+| 1:1 Видео | WebRTC P2P + TURN | Все сценарии |
+| 1:1 Голос | WebRTC P2P + TURN | Все сценарии |
+| Групповой | WebRTC Mesh | До 6 участников |
-> **Without credentials**: the server falls back to STUN only (Google + Cloudflare public STUN). Calls work on LAN without any extra configuration.
-
-### Call Types
-
-| Type | Transport | Recommended for |
-|------|-----------|-----------------|
-| 1:1 Video Call | WebRTC P2P + TURN | All scenarios |
-| 1:1 Voice Call | WebRTC P2P + TURN | All scenarios |
-| Group Video / Voice | WebRTC Mesh (full-mesh) | Up to 6 participants |
+> **Без настройки**: только STUN. Звонки в ЛВС работают без дополнительных настроек.
---
-## Push Notification Configuration
+## Push-уведомления
-Offline message notifications are delivered through **two channels** for maximum delivery rate:
-
-| Channel | Platforms | Configuration |
-|---------|-----------|---------------|
-| Web Push (VAPID) | Browsers (Chrome/Edge/Firefox) + iOS PWA (Safari 16.4+) | VAPID keys |
-| OneSignal | Native Android/iOS apps via Median.co | OneSignal App ID + REST Key |
-
-### Configuring Web Push
-
-1. Generate VAPID keys (one-time):
+| Канал | Платформы | Настройка |
+|-------|-----------|-----------|
+| Web Push | Браузеры + iOS PWA (Safari 16.4+) | Ключи VAPID |
+| OneSignal | Нативные приложения Median.co | App ID + REST Key |
```bash
-cd server
npx web-push generate-vapid-keys
```
-2. Add to `server/.env`:
+---
-```env
-VAPID_PUBLIC_KEY=your_public_key_here
-VAPID_PRIVATE_KEY=your_private_key_here
-VAPID_SUBJECT=mailto:admin@your-domain.com
-```
+## iOS — Постоянная установка без сертификата
-3. Restart the server — users can enable notifications from the Settings page
-
-> **iOS users** must first "Add to Home Screen" (PWA), and only iOS 16.4+ is supported.
-
-### Configuring OneSignal (Median.co Native Apps)
-
-1. Create an app on [OneSignal Dashboard](https://onesignal.com) and configure Firebase
-2. Enable OneSignal in Median.co and enter the App ID
-3. Add the OneSignal **App ID** and **REST API Key** to `server/.env`:
-
-```env
-ONESIGNAL_APP_ID=your_onesignal_app_id
-ONESIGNAL_REST_KEY=your_onesignal_rest_api_key
-```
-
-> **When not configured**: push notifications are silently disabled — all other features work normally.
+1. Развернуть на HTTPS-сервере → 2. Открыть в Safari → 3. Поделиться ⬆️ → 4. «На экран Домой»
---
-## iOS — Permanent No-Cert Deployment
+## Модель безопасности
-1. Deploy to a server with an HTTPS domain (required for WebRTC and Web Crypto APIs)
-2. Open `https://your.domain.com` in **Safari**
-3. Tap the Share button ⬆️ at the bottom of the screen
-4. Select **Add to Home Screen** → **Add**
-
-The app behaves identically to a native app — no Apple enterprise certificate required, no expiry.
-
----
-
-## Production Deployment (Nginx)
-
-```nginx
-server {
- listen 443 ssl http2;
- server_name your.domain.com;
-
- ssl_certificate /path/to/cert.pem;
- ssl_certificate_key /path/to/key.pem;
-
- # Frontend static files
- location / {
- root /path/to/paperphone/client;
- try_files $uri /index.html;
- }
-
- # REST API
- location /api/ {
- proxy_pass http://localhost:3000;
- proxy_set_header Host $host;
- }
-
- # WebSocket (messaging + call signalling)
- location /ws {
- proxy_pass http://localhost:3000;
- proxy_http_version 1.1;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection "upgrade";
- proxy_read_timeout 3600s;
- }
-}
+```
+Регистрация: IK + SPK + 10×OPK генерируются локально, публичные ключи загружаются
+Сообщение: Эфемерный ECDH → X25519 → XSalsa20-Poly1305
+Сервер видит: ✅ шифротекст + метаданные маршрутизации ❌ открытый текст / закрытые ключи
```
---
-## Project Structure
+## Схема базы данных
-```
-paperphone/
-├── docker-compose.yml
-├── server/
-│ ├── .env # Environment variables (incl. Cloudflare TURN keys)
-│ └── src/
-│ ├── app.js # Express application
-│ ├── routes/
-│ │ ├── auth.js # Register / Login (incl. X3DH public key upload)
-│ │ ├── users.js # User search / Prekey bundle download
-│ │ ├── friends.js # Friend requests / Accept (incl. offline push)
-│ │ ├── groups.js # Group management
-│ │ ├── messages.js # Historical messages (paginated ciphertext)
-│ │ ├── upload.js # Cloudflare R2 file upload
-│ │ ├── files.js # File proxy (when R2_PUBLIC_URL is not set)
-│ │ ├── moments.js # Moments feed (posts / likes / comments)
-│ │ ├── calls.js # TURN credential issuance
-│ │ └── push.js # Push subscription mgmt (Web Push + OneSignal)
-│ ├── services/
-│ │ ├── push.js # Web Push VAPID service
-│ │ └── onesignal.js # OneSignal REST API service
-│ └── ws/
-│ └── wsServer.js # WebSocket router (messages + call signalling + offline push)
-│
-└── client/
- ├── index.html # SPA entry + PWA meta + Median push bridge
- ├── manifest.json # PWA manifest
- ├── sw.js # Service Worker (offline cache + push notifications)
- └── src/
- ├── style.css # Premium design system (dark/light, glassmorphism)
- ├── app.js # Router + global state + incoming call listener
- ├── api.js # HTTP client
- ├── socket.js # WebSocket client (auto-reconnect)
- ├── i18n.js # Multi-language engine (zh / en / ja / ko / fr / de / ru / es)
- ├── services/
- │ ├── webrtc.js # WebRTC manager — CallManager class
- │ └── pushNotification.js # Push subscription mgmt (Web Push + Median bridge)
- ├── crypto/
- │ ├── ratchet.js # X3DH + Double Ratchet + ML-KEM-768
- │ └── keystore.js # IndexedDB private key store
- ├── pages/
- │ ├── login.js # Login / Register (key generation, language picker)
- │ ├── chats.js # Chat list
- │ ├── chat.js # Chat window (E2E encryption, call buttons)
- │ ├── groups.js # Group list (create group, search)
- │ ├── groupInfo.js # Group info (member management, DND, leave/disband)
- │ ├── contacts.js # Contacts (friend requests, online status)
- │ ├── discover.js # Discover page
- │ ├── profile.js # Me / Settings (language, fingerprint, notifications, PWA)
- │ └── call.js # Call UI (incoming / active / multi-party video)
-```
+11 таблиц, создаются автоматически при первом запуске:
+
+| Таблица | Назначение |
+|---------|------------|
+| `users` | Профили + публичные ключи ECDH/OPK |
+| `prekeys` | Одноразовые предключи X3DH |
+| `friends` | Связи дружбы |
+| `groups` / `group_members` | Группы + участники |
+| `messages` | Зашифрованные сообщения |
+| `moments` / `moment_images` | Посты + изображения |
+| `moment_likes` / `moment_comments` | Лайки + комментарии |
+| `push_subscriptions` | Web Push (VAPID) |
+| `onesignal_players` | OneSignal устройства |
---
-## Database Schema
+## Переменные окружения
-11 tables, auto-created on first server startup (`CREATE TABLE IF NOT EXISTS`):
-
-| Table | Purpose |
-|-------|---------|
-| `users` | User profiles + ECDH/OPK public keys |
-| `prekeys` | X3DH one-time prekey pool |
-| `friends` | Friendship relationships (pending / accepted / blocked) |
-| `groups` / `group_members` | Group chats + membership (incl. mute/DND status) |
-| `messages` | Encrypted payloads (offline buffer, deletable after delivery) |
-| `moments` | Social posts (text ≤ 1024 chars) |
-| `moment_images` | Post images (up to 9 per post) |
-| `moment_likes` | Likes (unique per user per post) |
-| `moment_comments` | Comments (≤ 512 chars each) |
-| `push_subscriptions` | Web Push subscriptions (VAPID) |
-| `onesignal_players` | OneSignal device registrations (Median.co) |
+| Переменная | Описание | По умолчанию |
+|------------|----------|-------------|
+| `PORT` | Порт сервера | `3000` |
+| `JWT_SECRET` | Ключ подписи JWT (**изменить в продакшене**) | dev_secret |
+| `DB_HOST`/`DB_PASS`/`DB_NAME` | Подключение MySQL | — |
+| `REDIS_HOST`/`REDIS_PASS` | Подключение Redis | — |
+| `R2_ACCOUNT_ID` | ID аккаунта Cloudflare | — |
+| `R2_ACCESS_KEY_ID` | Ключ доступа R2 API | — |
+| `R2_SECRET_ACCESS_KEY` | Секретный ключ R2 API | — |
+| `R2_BUCKET` | Имя бакета R2 | — |
+| `R2_PUBLIC_URL` | Публичный URL R2 (опц.) | — |
+| `CF_CALLS_APP_ID` | Calls App ID (опц.) | — |
+| `CF_CALLS_APP_SECRET` | Calls Secret (опц.) | — |
+| `VAPID_PUBLIC_KEY` | Публичный ключ VAPID (опц.) | — |
+| `VAPID_PRIVATE_KEY` | Закрытый ключ VAPID (опц.) | — |
+| `ONESIGNAL_APP_ID` | OneSignal App ID (опц.) | — |
+| `ONESIGNAL_REST_KEY` | OneSignal REST Key (опц.) | — |
---
-## Security Model
-
-```
-On Registration:
- Device generates IK (Identity Key) + SPK (Signed PreKey) + 10× OPK (One-Time PreKeys)
- Public keys are uploaded; private keys stay on-device (four-tier persistence)
-
-On Each Message:
- Sender fetches recipient's IK public key
- Generates a fresh ephemeral ECDH keypair (per-message)
- X25519 ECDH → shared secret → XSalsa20-Poly1305 encrypt
- Ephemeral public key sent in message header; destroyed after use
-
-What the Server Sees:
- ✅ Ciphertext blob + routing metadata (sender / recipient UUID, timestamps)
- ❌ Plaintext / private keys / ephemeral keys / call content
-```
-
----
-
-## Environment Variables
-
-| Variable | Description | Default |
-|----------|-------------|---------|
-| `PORT` | Server port | `3000` |
-| `JWT_SECRET` | JWT signing key (**change in production**) | dev_secret |
-| `DB_HOST` / `DB_PASS` / `DB_NAME` | MySQL connection | — |
-| `REDIS_HOST` / `REDIS_PASS` | Redis connection | — |
-| `R2_ACCOUNT_ID` | Cloudflare account ID | — |
-| `R2_ACCESS_KEY_ID` | R2 API token access key | — |
-| `R2_SECRET_ACCESS_KEY` | R2 API token secret key | — |
-| `R2_BUCKET` | R2 bucket name | — |
-| `R2_PUBLIC_URL` | R2 public base URL (optional) — enables direct CDN links | — |
-| `CF_CALLS_APP_ID` | Cloudflare Calls App ID (optional) | — |
-| `CF_CALLS_APP_SECRET` | Cloudflare Calls App Secret (optional) | — |
-| `VAPID_PUBLIC_KEY` | Web Push VAPID public key (optional) | — |
-| `VAPID_PRIVATE_KEY` | Web Push VAPID private key (optional) | — |
-| `VAPID_SUBJECT` | VAPID contact email (optional) | `mailto:admin@paperphone.app` |
-| `ONESIGNAL_APP_ID` | OneSignal App ID (optional, for Median.co) | — |
-| `ONESIGNAL_REST_KEY` | OneSignal REST API Key (optional) | — |
-
----
-
-## License
+## Лицензия
MIT © PaperPhone Contributors
diff --git a/client/nginx.conf b/client/nginx.conf
index 0da0e4b..6b9cdba 100644
--- a/client/nginx.conf
+++ b/client/nginx.conf
@@ -1,4 +1,13 @@
-# Nginx config for PaperPhone — serves client and proxies API to server
+# ─────────────────────────────────────────────────────────────────
+# Nginx config for PaperPhone — serves PWA + reverse-proxy to API
+#
+# This file is an envsubst template. At container start the
+# Dockerfile CMD substitutes ${SERVER_URL} and writes the final
+# config to /etc/nginx/conf.d/paperphone.conf.
+#
+# Docker Compose: SERVER_URL = http://server:3000
+# Zeabur: SERVER_URL = ${ZEABUR_WEB_URL} (e.g. http://10.43.x.x:3000)
+# ─────────────────────────────────────────────────────────────────
server {
listen 80;
@@ -7,39 +16,42 @@ server {
root /usr/share/nginx/html;
index index.html;
- # Gzip compression
+ # ── Compression ──────────────────────────────────────────────
gzip on;
gzip_types text/plain text/css application/javascript application/json application/wasm;
gzip_min_length 1024;
+ gzip_vary on;
- # Security headers
+ # ── Security headers ─────────────────────────────────────────
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "no-referrer" always;
- # Cache PWA assets aggressively (content-hashed in prod)
+ # ── Static asset caching ─────────────────────────────────────
location ~* \.(css|js|wasm|png|jpg|webp|ico|svg|woff2?)$ {
expires 7d;
add_header Cache-Control "public, max-age=604800, immutable";
}
- # Service worker — no-cache so updates propagate immediately
+ # Service worker — always revalidate so updates propagate
location = /sw.js {
add_header Cache-Control "no-cache, no-store, must-revalidate";
expires 0;
}
- # WebSocket upgrade — proxy to backend
+ # ── WebSocket — proxy to backend ─────────────────────────────
location /ws {
proxy_pass ${SERVER_URL};
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 3600s;
+ proxy_send_timeout 3600s;
}
- # REST API — proxy to backend
+ # ── REST API — proxy to backend ──────────────────────────────
location /api/ {
proxy_pass ${SERVER_URL};
proxy_http_version 1.1;
@@ -48,14 +60,18 @@ server {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
client_max_body_size 51m;
+
+ # Long-poll & upload timeout
+ proxy_read_timeout 120s;
+ proxy_send_timeout 120s;
}
- # Health check from backend
+ # ── Health check (forwarded to backend) ──────────────────────
location /health {
proxy_pass ${SERVER_URL};
}
- # SPA fallback — all other routes serve index.html
+ # ── SPA fallback ─────────────────────────────────────────────
location / {
try_files $uri $uri/ /index.html;
}
diff --git a/docker-compose.yml b/docker-compose.yml
index dab965d..e164759 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,5 +1,3 @@
-version: "3.9"
-
# ─────────────────────────────────────────────────────────────────
# PaperPhone — Full-Stack Docker Compose
#
@@ -8,14 +6,17 @@ version: "3.9"
# server — Node.js API + WebSocket
# mysql — Message store / user DB
# redis — Online presence & session cache
-# minio — S3-compatible object storage for media uploads
+#
+# File storage:
+# Cloudflare R2 (configured via R2_* env vars in server/.env)
+# No MinIO / S3 container needed.
#
# Images:
# docker pull facilisvelox/paperphone-client:latest
# docker pull facilisvelox/paperphone-server:latest
#
# Quick start:
-# cp server/.env.example server/.env # edit secrets & CF_CALLS_APP_ID
+# cp server/.env.example server/.env # edit secrets & R2 keys
# docker compose up -d
# ─────────────────────────────────────────────────────────────────
@@ -26,6 +27,8 @@ services:
image: facilisvelox/paperphone-client:latest
ports:
- "80:80"
+ environment:
+ SERVER_URL: http://server:3000
depends_on:
server:
condition: service_healthy
@@ -40,17 +43,16 @@ services:
environment:
DB_HOST: mysql
DB_PORT: 3306
+ DB_USER: ${DB_USER:-paperphone}
+ DB_PASS: ${DB_PASS:-changeme}
+ DB_NAME: paperphone
REDIS_HOST: redis
REDIS_PORT: 6379
- MINIO_ENDPOINT: minio
- MINIO_PORT: 9000
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
- minio:
- condition: service_started
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
interval: 30s
@@ -97,30 +99,9 @@ services:
networks:
- paperphone
- # ── MinIO (S3-compatible) ─────────────────────────────────────
- minio:
- image: minio/minio:latest
- command: server /data --console-address ":9001"
- environment:
- MINIO_ROOT_USER: ${MINIO_ACCESS_KEY:-minioadmin}
- MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY:-minioadmin}
- volumes:
- - minio_data:/data
- ports:
- - "9001:9001" # MinIO console (remove in prod if not needed)
- healthcheck:
- test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
- interval: 30s
- timeout: 5s
- retries: 5
- restart: unless-stopped
- networks:
- - paperphone
-
volumes:
mysql_data:
redis_data:
- minio_data:
networks:
paperphone:
diff --git a/server/.env b/server/.env
index 8b932b7..1c96307 100644
--- a/server/.env
+++ b/server/.env
@@ -10,12 +10,13 @@ DB_NAME=paperphone
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASS=
-MINIO_ENDPOINT=localhost
-MINIO_PORT=9000
-MINIO_USE_SSL=false
-MINIO_ACCESS_KEY=minioadmin
-MINIO_SECRET_KEY=minioadmin
-MINIO_BUCKET=paperphone
+
+# Cloudflare R2 (S3-compatible object storage)
+R2_ACCOUNT_ID=
+R2_ACCESS_KEY_ID=
+R2_SECRET_ACCESS_KEY=
+R2_BUCKET=paperphone
+R2_PUBLIC_URL=
# Cloudflare Calls (TURN service)
# Get from: Cloudflare Dashboard → Calls → Your App → API Keys
diff --git a/server/.env.docker b/server/.env.docker
index 771788f..917f4f5 100644
--- a/server/.env.docker
+++ b/server/.env.docker
@@ -1,5 +1,5 @@
# ─────────────────────────────────────────────────────────────────
-# PaperPhone — Production Environment Variables
+# PaperPhone — Production Environment Variables (Docker Compose)
# Copy this file to server/.env and fill in real values
# ─────────────────────────────────────────────────────────────────
@@ -13,7 +13,7 @@ JWT_SECRET=CHANGE_ME_USE_OPENSSL_RAND_HEX_32
JWT_EXPIRES_IN=7d
# ── MySQL ─────────────────────────────────────────────────────────
-# In Docker, DB_HOST is the service name: mysql
+# In Docker Compose, DB_HOST is the service name: mysql
DB_HOST=mysql
DB_PORT=3306
DB_USER=paperphone
@@ -21,17 +21,30 @@ DB_PASS=CHANGE_ME_DB_PASSWORD
DB_NAME=paperphone
# ── Redis ─────────────────────────────────────────────────────────
-# In Docker, REDIS_HOST is the service name: redis
+# In Docker Compose, REDIS_HOST is the service name: redis
REDIS_HOST=redis
REDIS_PORT=6379
-# Optional password (matches redis command --requirepass)
REDIS_PASS=
-# ── MinIO ─────────────────────────────────────────────────────────
-# In Docker, MINIO_ENDPOINT is the service name: minio
-MINIO_ENDPOINT=minio
-MINIO_PORT=9000
-MINIO_USE_SSL=false
-MINIO_ACCESS_KEY=CHANGE_ME_MINIO_ACCESS
-MINIO_SECRET_KEY=CHANGE_ME_MINIO_SECRET
-MINIO_BUCKET=paperphone
+# ── Cloudflare R2 (S3-compatible object storage) ─────────────────
+# Get from: Cloudflare Dashboard → R2 → Manage API Tokens
+R2_ACCOUNT_ID=
+R2_ACCESS_KEY_ID=
+R2_SECRET_ACCESS_KEY=
+R2_BUCKET=paperphone
+R2_PUBLIC_URL=
+
+# ── Cloudflare TURN (optional — for cross-network WebRTC calls) ──
+# Get from: Cloudflare Dashboard → Workers & Pages → Calls
+CF_CALLS_APP_ID=
+CF_CALLS_APP_SECRET=
+
+# ── Web Push / VAPID (optional) ──────────────────────────────────
+# Generate with: npx web-push generate-vapid-keys
+VAPID_PUBLIC_KEY=
+VAPID_PRIVATE_KEY=
+VAPID_SUBJECT=mailto:admin@paperphone.app
+
+# ── OneSignal (optional — Median.co native push) ─────────────────
+ONESIGNAL_APP_ID=
+ONESIGNAL_REST_KEY=
diff --git a/server/.env.example b/server/.env.example
index 2abb183..5409191 100644
--- a/server/.env.example
+++ b/server/.env.example
@@ -18,21 +18,26 @@ REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASS=
-# MinIO
-MINIO_ENDPOINT=localhost
-MINIO_PORT=9000
-MINIO_USE_SSL=false
-MINIO_ACCESS_KEY=minioadmin
-MINIO_SECRET_KEY=minioadmin
-MINIO_BUCKET=paperphone
+# Cloudflare R2 (S3-compatible object storage)
+# Get from Cloudflare Dashboard → R2 → Manage API Tokens
+R2_ACCOUNT_ID=
+R2_ACCESS_KEY_ID=
+R2_SECRET_ACCESS_KEY=
+R2_BUCKET=paperphone
+R2_PUBLIC_URL=
-# Web Push (VAPID)
+# Cloudflare TURN (optional — for cross-network WebRTC calls)
+# Get from Cloudflare Dashboard → Workers & Pages → Calls
+CF_CALLS_APP_ID=
+CF_CALLS_APP_SECRET=
+
+# Web Push (VAPID) — optional
# Generate with: npx web-push generate-vapid-keys
VAPID_PUBLIC_KEY=
VAPID_PRIVATE_KEY=
VAPID_SUBJECT=mailto:admin@paperphone.app
-# OneSignal (Median.co native push)
+# OneSignal (Median.co native push) — optional
# Get from OneSignal Dashboard → Settings → Keys & IDs
ONESIGNAL_APP_ID=
ONESIGNAL_REST_KEY=
diff --git a/zeabur.yaml b/zeabur.yaml
index f348a9a..9499418 100644
--- a/zeabur.yaml
+++ b/zeabur.yaml
@@ -25,10 +25,11 @@ spec:
## Features
- 📹 **Video & Voice Calls** — WebRTC P2P (1:1) + Mesh (group up to 6)
- - 🌐 **Multi-language** — Chinese, English, Japanese, Korean, French
+ - 🌐 **Multi-language** — Chinese, English, Japanese, Korean, French, German, Russian, Spanish
- 📱 **iOS PWA** — "Add to Home Screen" via Safari, no enterprise cert needed
- 💬 Rich messaging: text, images, voice, emoji, delivery receipts, typing indicators
- 🗂️ **Cloudflare R2** file storage (images & voice messages)
+ - 🏷️ **Friend Tags** — tag-based contact filtering & Moments visibility control
## After Deployment
1. Open the domain assigned to the **client** service.
@@ -386,10 +387,11 @@ localization:
## 功能亮点
- 📹 **视频/语音通话** — WebRTC P2P(1:1)+ Mesh 多人(≤6 人)
- - 🌐 **多语言** — 中文、英文、日语、韩语、法语
+ - 🌐 **多语言** — 中文、英文、日语、韩语、法语、德语、俄语、西班牙语
- 📱 **iOS 永久免签** — Safari「添加到主屏幕」,无需企业证书
- 💬 富文本消息、图片、语音、Emoji、送达回执、打字状态
- 🗂️ **Cloudflare R2** 对象存储(图片与语音消息)
+ - 🏷️ **好友标签** — 标签分类筛选通讯录 + 朋友圈可见性控制
## 部署后操作
1. 打开分配给 **client** 服务的域名。