03 / Projects

Messages

A self-hosted service for encrypted, expiring, and burn-after-reading messages with private replies and invitation-based registration.

Nested cobalt and paper-white envelopes surround an acid-lime message core, with guarded blue routes branching toward smaller envelopes and fragments dissolving along the lower edge.
StateCompleted
AccessPublic

Private messages with an ending built in

Messages is a self-hosted service for encrypted notes shared through one-time links. A registered user writes a message, optionally chooses a password and lifetime, and shares the URL. Notes expire after 72 hours by default, with options from one hour to 30 days or immediate deletion after the first successful read.

Encryption is product behaviour

Every message and reply has a random content key. AES-256-GCM encrypts the body, and SQLite stores only authenticated ciphertext envelopes. For a password-protected note, a key derived with scrypt participates in wrapping the content key. The server never stores that password and cannot decrypt the body from the master key and a complete database dump alone.

Expiry is not merely delegated to a background timer. A sweeper deletes expired records every minute, and every read checks the deadline again. Stale content is therefore never served even when cleanup is delayed. Burn-after-reading takes effect only after successful decryption; a wrong password cannot destroy the note.

Replies and controlled growth

Recipients can reply privately through the same link. Only the original message owner sees replies, and every reply has its own optional password and lifetime. Registration is semi-invite-only: a valid shared message acts as an invitation, while one seeded account bootstraps the first note.

Signed-in users have history with live reply counts, can edit options or remove notes manually, change their password, and delete their account together with its content. The responsive interface is available in German and English and supports light, dark, or system colour schemes.

A small, direct architecture

One Node.js process serves both the Express API and built Vue 3 SPA. The frontend uses Vite, Vue Router, Pinia, and vue-i18n. The backend combines Express, Zod request validation, better-sqlite3, JWT sessions in HttpOnly cookies, and bcrypt account passwords. Persistence deliberately remains one SQLite file in WAL mode on a durable volume.

Helmet provides a strict same-origin CSP. Authentication and message views are rate-limited, and production cookies are Secure and SameSite=Lax. Encryption and JWT keys live in Ansible Vault and deploy as a protected environment file; careless rotation of the master key would intentionally make existing notes unreadable and is documented as an operational boundary.

Production is a single Docker Swarm service behind Traefik with TLS, a health check, and start-first rollout. It is deliberately smaller than the other projects: few moving parts, one clear security contract, and a complete API smoke test. The service is live at messages.dphillips.de.