A full-stack, end-to-end encrypted messenger combining a modern .NET 10 API backend with a React Native mobile application. Built with security at its core, featuring Signal Protocol E2E encryption, challenge-response authentication, and multi-platform support.
🔒 Production-ready security | 📱 React Native mobile | 🔐 Signal Protocol E2E | 🐳 Docker support
- Quick Start
- Tech Stack
- Features
- Architecture
- Project Structure
- Security Model
- API Endpoints
- Development
- Docker
- Troubleshooting
- Contributing
- License
- Node.js ≥ 22.11.0
- .NET 8.0+ (tested on .NET 10)
- Docker & Docker Compose (optional)
- PostgreSQL (if running without Docker)
- Android SDK or Xcode (for mobile builds)
# Clone repository
git clone https://github.com/your-org/messager.git
cd messager
# Copy environment template
cp .env.example .env
# Update .env with your values
nano .env
# Start Docker stack
docker-compose up -d
# Apply migrations (inside docker)
docker exec messager-api dotnet ef database update
# Access the app
# - API: https://localhost:443/swagger
# - Mobile: http://localhost:3000 (dev server)cd API
dotnet restore
dotnet build
dotnet runRuns on http://localhost:5000 (HTTPS redirect enabled).
cd App
npm install
npm start # Start Metro bundler
npm run android # Android emulator/device
npm run ios # iOS simulator/device| Layer | Technology | Notes |
|---|---|---|
| Backend API | .NET 10, ASP.NET Core Minimal APIs | Clean Architecture (Domain → Application → Infrastructure) |
| Database | PostgreSQL 17 + Entity Framework Core | Type-safe, migrations-ready |
| Authentication | JWT + RSA-OAEP challenge-response | 12-hour token TTL, 30s clock skew tolerance |
| E2E Encryption | Signal Protocol (libsignal-protocol-typescript) | Ratcheting, forward secrecy |
| Message Encryption | AES-256-GCM + RSA-OAEP | Authenticated, random IVs |
| Mobile | React Native 0.85 + TypeScript | Cross-platform iOS/Android |
| Local Storage | SQLite + AsyncStorage | Persistent offline message cache |
| Biometrics | react-native-keychain | Fingerprint/Face + secure key storage |
| Infrastructure | Docker, Nginx, Docker Compose | TLS 1.2+, WebSocket proxy, HTTP→HTTPS redirect |
- ✅ End-to-End Encrypted Messaging — Signal Protocol with forward secrecy
- ✅ Zero-Knowledge Architecture — Server never sees plaintext messages
- ✅ Challenge-Response Login — RSA-SHA512 signatures, no password transmission
- ✅ Real-time Sync — WebSocket gateway for instant notifications
- ✅ Multi-Profile Support — Manage multiple identities on one device
- ✅ Offline-First — SQLite local cache, automatic sync on reconnection
- ✅ Biometric Authentication — PIN + fingerprint/face unlock
- ✅ Message TTL — Automatic expiration (configurable, default 30 days)
- ✅ Auto-Lock — Session timeout with background inactivity
- ✅ Cross-Platform — iOS and Android support via React Native
Domain Layer
↓
Application Layer (CQRS-style handlers)
↓
Infrastructure Layer (EF Core, services)
↓
API Layer (HTTP endpoints, WebSocket hubs)
1. User registers: RSA key pair generated, encrypted with PIN (PBKDF2)
2. Login: Server sends cryptographic challenge
3. Client signs challenge with private key
4. Server verifies signature, issues JWT
5. JWT used for all subsequent requests (WebSocket + HTTP)
Sender Receiver
↓ ↓
AES-256-GCM Signal Protocol
↓ ↓
RSA-OAEP (recipient's key) Decrypt with ratcheted key
↓ ↓
[Encrypted message] --------→ Store in local cache
Messager/
├── API/ # ASP.NET Core Minimal APIs
│ ├── Endpoints/ # Route handlers
│ │ ├── AuthEndpoints.cs
│ │ ├── MessageEndpoints.cs
│ │ ├── KeyExchangeEndpoints.cs
│ │ ├── PublicKeyEndpoints.cs
│ │ ├── SyncEndpoints.cs
│ │ └── PreKeyEndpoints.cs
│ ├── Contracts/ # Request/Response DTOs
│ ├── Security/
│ │ └── JwtTokenIssuer.cs # JWT generation (12h TTL)
│ ├── Realtime/
│ │ └── SyncNotificationHub.cs # WebSocket gateway
│ ├── BackgroundServices/
│ │ └── MessageCleanupService.cs # TTL-based expiration
│ ├── Program.cs # DI, middleware setup
│ └── Dockerfile
│
├── Application/ # Business logic (CQRS handlers)
│ ├── Handlers/
│ │ ├── RegisterHandler.cs
│ │ ├── LoginHandler.cs
│ │ ├── GetLoginChallengeHandler.cs
│ │ ├── SendMessageHandler.cs
│ │ ├── GetMessagesHandler.cs
│ │ ├── SendKeyExchangeHandler.cs
│ │ └── GetKeyExchangesHandler.cs
│ └── Interfaces/ # Service contracts
│
├── Infrastructure/ # Data access & services
│ ├── Persistence/
│ │ └── MessagerDbContext.cs # EF Core DbContext
│ ├── Repositories/
│ │ └── PublicKeyRepository.cs
│ ├── Services/
│ │ ├── LoginService.cs
│ │ ├── LoginChallengeService.cs
│ │ ├── CurrentPublicKeyAccessor.cs
│ │ └── PublicKeySecurityService.cs
│ └── Migrations/ # EF Core migrations
│
├── Domain/ # Core entities & value objects
│ ├── BaseEntity.cs
│ ├── PublicKey.cs
│ ├── Message.cs
│ ├── KeyExchange.cs
│ ├── LoginChallenge.cs
│ ├── SignedPreKey.cs
│ └── OneTimePreKey.cs
│
├── App/ # React Native frontend
│ ├── src/
│ │ ├── pages/ # Screens (Auth, Messaging, Security, etc.)
│ │ ├── components/ # Reusable UI components
│ │ ├── services/ # API clients, crypto, storage
│ │ ├── context/ # React Context (PrivateKeySessionContext)
│ │ ├── types/ # TypeScript definitions
│ │ └── App.tsx # Root component
│ ├── android/ # Android native code
│ ├── ios/ # iOS native code
│ ├── package.json
│ ├── tsconfig.json
│ ├── Dockerfile.android
│ └── metro.config.js
│
├── docker-compose.yml # PostgreSQL + API + Nginx + Android builder
├── nginx/ # Nginx configuration & SSL setup
├── .env.example # Environment variables template
├── CLAUDE.md # Development guidelines
├── DOKUMENTACJA.md # Polish documentation
├── doc/ # Multilingual docs (en, pl, de, fr, es, pt, ua)
└── Messager.slnx # Modern .NET solution format
-
Key Exchange — RSA-OAEP (2048-bit)
- Asymmetric encryption of symmetric keys
- Server stores recipient's public key only
-
Message Encryption — AES-256-GCM
- Authenticated encryption with random IVs
- Forward secrecy via Signal Protocol ratcheting
-
Authentication — RSA-SHA512 + Challenge-Response
- No passwords transmitted over network
- Challenge prevents replay attacks
- Signatures prove key ownership
- Private keys: Encrypted with PBKDF2 (PIN-derived key)
- Stored in SQLite on-device (iOS Keychain, Android Keystore)
- Messages: AES-256-GCM in database (server never sees plaintext)
- HTTPS mandatory (TLS 1.2+)
- Nginx redirect: HTTP → HTTPS
- WebSocket over WSS (secure)
- JWT validation on all endpoints
- JWT includes
issuerandaudienceclaims (validated) - Rate limiting on auth (10 req/min/IP) and search (30 req/min/IP)
- SQL injection protection: EF Core parameterized queries
- LIKE wildcards escaped in user search
- Fingerprint validation: 128-char hex format
- DEV: 1024-bit RSA (switch to 2048-bit in production)
- DEV: Self-signed SSL certificates in
ssl/(use real certs in production) - Ensure
JWT_SIGNING_KEYandPOSTGRES_CONNECTION_STRINGare set (no fallbacks)
POST /api/auth/register # Create account
POST /api/auth/login/challenge # Get RSA challenge
POST /api/auth/login # Verify signature, get JWTPOST /api/messages # Send encrypted message
GET /api/messages/{peerFingerprint} # Get message historyPOST /api/key-exchanges # Send key exchange
GET /api/key-exchanges/{peerFingerprint}# Get pending exchangesPOST /api/prekeys/signed # Upload signed pre-key
POST /api/prekeys/one-time # Upload one-time pre-keys
GET /api/prekeys/one-time/{fingerprint} # Fetch one-time keyGET /api/public-keys/{fingerprint} # Retrieve peer's public key
GET /api/public-keys/search # Search users by fingerprint (paginated)
GET /api/public-keys/verify/{fingerprint} # Verify key existsWebSocket /ws/conversations/{peerFingerprint}?access_token=JWT
• Query: real-time notifications
• Payload: sync events (new messages, key exchanges)
• Keep-alive: 30s
• Timeout: 3600s (1h)
cd API
dotnet restore
dotnet build
# Run tests
dotnet test
# Run with live reload
dotnet watch run
# Publish for production
dotnet publish -c Release -o ./publishcd App
# Install dependencies
npm install
# Lint code
npm run lint
# Run tests
npm test
# Build AAB (Android App Bundle)
cd android && ./gradlew bundleRelease
# Build IPA (iOS)
cd ios && xcodebuild archive-
Pages (
src/pages/)AuthGatewayPage— Login/register entryRegistrationPage— RSA key generationLocalLoginPage— PIN-based unlockMessagingPage— Contact list & searchConversationPage— Chat UISecuritySettingsPage— Key management
-
Services (
src/services/)authApi.ts— REST client for/api/auth/*messagingApi.ts— REST + WebSocket for messagessignalStore.ts— Signal Protocol E2E statechatCrypto.ts— AES-256-GCM, RSA-OAEPregistrationCrypto.ts— Key generation, PBKDF2biometricAuth.ts— Fingerprint/Face unlockchatStore.ts— SQLite message cacheprofileStore.ts— Multi-profile management
-
Context (
src/context/)PrivateKeySessionContext— In-memory unlocked key, auto-lock on inactivityLoadingOverlayContext— Global loading spinner
# API
JWT_SIGNING_KEY=<base64-encoded-key>
JWT_AUDIENCE=messager-client
JWT_ISSUER=messager-api
POSTGRES_CONNECTION_STRING=Server=db;Database=messager;Username=postgres;Password=...
MESSAGE_TTL_DAYS=30
# Mobile & Nginx
APP_PORT=443
APP_DOMAIN=localhost
MESSAGER_API_BASE_URL=https://localhost:443
NGINX_USE_SSL=true
SSL_CA_CERT_FILE=ca.pem| Service | Image | Port | Notes |
|---|---|---|---|
| db | postgres:17-alpine | 5432 | Volume: db-data, health checks |
| api | custom (API/Dockerfile) | 5000 | ASP.NET Core, auto-migrated |
| nginx | nginx:alpine | 80, 443 | Reverse proxy, SSL, WebSocket |
| android-builder | custom (App/Dockerfile.android) | — | Release builds only |
# Up and running
docker-compose up -d
# View logs
docker-compose logs -f api
# Stop
docker-compose down
# Rebuild images
docker-compose build --no-cache# Automatic on first run (EnsureCreated)
# To manually apply migrations:
docker exec messager-api dotnet ef database update
# Drop and recreate (dev only)
docker exec messager-api dotnet ef database drop --force
docker exec messager-api dotnet ef database updatePlace certificates in ssl/:
ssl/
├── server.pem # Server certificate
├── server-key.pem # Private key
└── ca.pem # CA certificate
Set NGINX_USE_SSL=true in .env.
# Check JWT_SIGNING_KEY is set
echo $JWT_SIGNING_KEY
# Check database connection
dotnet user-secrets list
# View detailed logs
dotnet run --verbosity Debug- Ensure
MESSAGER_API_BASE_URLpoints to correct server - Check SSL certificate trust (development: add CA cert to app)
- Verify private key is unlocked in
PrivateKeySessionContext - Check biometric permissions (iOS: Info.plist, Android: AndroidManifest.xml)
- Verify JWT token is valid (12h expiry)
- Check Nginx WebSocket timeout:
proxy_read_timeout 3600s; - Ensure
Access-Control-Allow-Originheader is set - Validate
peerFingerprintis 128-char hex
# View database logs
docker logs messager-db
# Connect to database
psql -h localhost -U postgres -d messager
# Check connection string
cat .env | grep POSTGRES# Add EF Core tooling (if missing)
dotnet tool install --global dotnet-ef
# List pending migrations
dotnet ef migrations list
# View migration status
dotnet ef database info- Fork the repository
- Create feature branch:
git checkout -b feature/your-feature - Make changes following CLAUDE.md guidelines
- Test locally (
npm test,dotnet test) - Commit with clear messages (see git history for style)
- Push and open a pull request
- Address review feedback
- Backend: Clean Architecture, async/await, EF Core best practices
- Frontend: TypeScript strict mode, functional components, React hooks
- Security: No hardcoded secrets, use environment variables
- Tests: Unit tests for critical logic, integration tests for APIs
This project is licensed under the MIT License — see LICENSE file for details.
- CLAUDE.md — Development guidelines
- DOKUMENTACJA.md — Polish documentation
- doc/ — Multilingual guides (English, Polish, German, French, Spanish, Portuguese, Ukrainian)
- Signal Protocol — E2E encryption specification
- ASP.NET Core Docs — Backend framework
- React Native Docs — Mobile framework
Q: Can I use this in production?
A: The architecture is production-ready, but this is currently a demo/personal project. Conduct security audits before production deployment.
Q: What's the message size limit?
A: Limited by MaxRequestBodySize in Nginx (~1MB by default).
Q: How long are messages stored?
A: MESSAGE_TTL_DAYS environment variable (default: 30 days).
Q: Can I run this without Docker?
A: Yes, but you'll need PostgreSQL, Node.js, and .NET installed locally.
Q: Is the database encrypted?
A: Messages are encrypted client-side; database stores only ciphertext. Add full-disk encryption for additional protection.
Built with ❤️ using .NET 10, React Native, and Signal Protocol.
For issues, questions, or contributions, please open an issue or pull request on GitHub.