§1The problem
I wanted to find out whether I could build a real messenger alone (chats, groups, media, real time) with its own visual identity, without relying on a ready-made backend service.
§2What I built
- Own serverHTTP API + WebSocket, SQLite database, media on disk.
- Single appExpo / React Native (iOS, Android and web), with the screens from the design prototype.
- MessagingReal-time chats, message receipts, sending and receiving photos, videos and files (up to 2 GB in the iPhone PWA).
- Web extrasDrag and drop (and paste) files into a chat.
- InvitesBy link/QR to find contacts (no phone-number login in v1).
- Long textSplit automatically (65,536-character limit per message).
- IdentityPer device, with the token stored only as a hash on the server; single-account mode for the beta.
- TestsAutomated tests on the server (API and WebSocket, with a temporary database) and on the app logic.
§3The hard parts
5 entries- 01
Real-time sync with offline sending
An outbox, receipts and a local cache that the app opens before validating the session.
- 02
Large media
Chunked, streamed upload (raw body, no multipart), with signed links (HMAC) so <img>/<video> work without an auth header.
- 03
Identity designed to evolve
“Who the user is” separated from “how they proved it”, so phone login can be added later without migrating data.
- 04
Same code on three platforms
Including real differences: blurred glass on iOS and web, a simplified version on Android; localStorage cache on the web and SQLite on the phone.
- 05
iPhone PWA quirks
Correct video duration and the photo picker.
§4Result
- server tests (API and WebSocket)
- 25
- max upload in the iPhone PWA
- 2 GB
The server and the web app have been live on my private network since October 2026, for a closed beta. The server has 25 API/WebSocket tests. Still missing: the installable phone build (it needs an Expo account and, on iOS, an Apple developer account) and phone-number login.
§5Stack
- Expo / React Native (expo-router)
- TypeScript
- Node 24
- Hono
- WebSocket
- SQLite (better-sqlite3, WAL mode)
- Zod
- Zustand
- sharp
- pm2
- Tailscale