A self-hosted PWA messenger without a phone number: DMs and groups, voice, files, push, reactions, WebRTC calls. Node + Socket.IO + SQLite with Litestream backups, a security pass, and a landing that lists what the product does not do.
Msgr is a messenger with no phone number, no e-mail, no profile form. A login and a password — and you're in. It opens in the browser and installs to the home screen like a regular app — bypassing the App Store and Google Play. Own server, no ads or analytics, free.
"Messengers love to promise military-grade encryption. So here it is, plainly and point by point: what's here and what isn't."
The project grew from a simple need: a small circle of people who want a chat without phone registration and foreign clouds. Hence three principles: no unnecessary data about the user, a ten-second install, honesty in promises.

DMs and groups with full history. Voice messages with a waveform player. Photos, video and files up to 50 MB — drag into the window or paste a screenshot with Cmd+V. Push notifications even with the tab closed — iPhone, Android, desktop. Reactions (up to three per message), quoted replies, forwarding, edit and delete for both sides, search and pinned messages, online / typing / read statuses, GIFs, link previews, WebRTC audio calls, a profile by short link.

// lib/sockets.js — a message: participant check, SQLite insert, room broadcast, push to whoever is offline
socket.on('send_message', async ({ conversationId, text, replyToId }) => {
if (!text?.trim() || !isParticipant(conversationId, userId)) return;
const msg = { id: uuidv4(), conversation_id: conversationId, author_id: userId,
type: 'text', content: String(text).slice(0, 4000), created_at: Date.now(),
reply_to_id: replyToId || null, reactions: {} };
q.insertMsgReply.run(msg.id, msg.conversation_id, msg.author_id, msg.type, msg.content, null, null, msg.created_at, msg.reply_to_id);
broadcastNewMessage(conversationId, msg);
notifyRecipients(conversationId, userId, msg);
});
It's a website that knows how to pretend to be an app: open the link, add to the home screen — and it lives there as an icon, with notifications. The service worker serves navigations network-first (updates arrive immediately), static assets with content hashes and immutable caching. iOS was its own job: PWA traps on file downloads, a push opening the wrong user's window, a push re-registration loop after Apple's 410 — all caught and fixed by hand.

The landing has two columns. What's true: registration without a number, HTTPS, no ads or trackers, attachments served only to chat participants, free. What's missing: end-to-end encryption — the server can see messages; e-mail recovery — forgot your password, message the admin. Below, an FAQ as a chat: "Do I have to download it?", "Who can read my messages?", "Are there calls?".


A dedicated security pass: CSP with nonces and security headers, a stored XSS via reactions closed, SSRF in link previews fixed, authorised attachment downloads, token revocation, a defused password reset, rate limiting on login and registration, /healthz, graceful shutdown with a WAL checkpoint.
Hosting is a self-managed server: systemd with auto-restart, Caddy with automatic TLS, Litestream streaming SQLite backups to S3, a LUKS disk-encryption guide and a one-shot bootstrap script for a clean Ubuntu. The move from Railway to self-hosted is documented step by step.
deploy/
bootstrap.sh # clean Ubuntu → a running Msgr in one command
Caddyfile # HTTPS, reverse proxy
msgr.service # systemd, auto-restart, SIGTERM → WAL checkpoint
litestream.yml # SQLite → S3 continuously: a dead disk ≠ lost history
Node.js · Express · Socket.IO · SQLite (better-sqlite3) · web-push (VAPID) · WebRTC · MediaRecorder · Service Worker · vanilla JS client (virtualised message list, lazy modules: calls, recorder, lightbox) · Caddy · systemd · Litestream
A messenger that does exactly what it promises: a chat for your own people, no phone, no stores, no clouds, on your own server with backups. Version 1.0.9, real users, and a landing page stripped of everything the product doesn't do.