The application must use end-to-end encryption. The VPS/server must never receive or store plaintext message contents. Message history must primarily live on users' local devices. The server should act as a temporary encrypted-message relay, account system, public-key directory, presence service, and encrypted attachment host.

Core product

Users have:

Unique immutable internal user IDs

Unique @usernames

Display names

Avatars

Multiple devices

Direct messages

Private groups with a maximum of 10 members

Message replies

Reactions

Typing indicators

Online/offline presence

File/image attachments

Optional disappearing messages

Device management

User blocking

Message reporting

Do not implement public servers, public channels, or groups larger than 10.

Architecture

Use this architecture:

CLIENT

Web or desktop UI

Local encrypted message database

Local crypto engine

Local device identity

Local conversation state

WebSocket connection to server

VPS SERVER

Authentication API

User/@username directory

Device/key directory

Conversation membership service

WebSocket encrypted-message relay

Temporary encrypted-message queue

Presence/typing service

Push notification service

Encrypted attachment storage interface

DATABASE

PostgreSQL

CACHE / REALTIME

Redis

FILE STORAGE

S3-compatible object storage or local MinIO

REVERSE PROXY

Caddy or nginx

TLS

TLS 1.3 where supported

Critical security rule

The server must NEVER receive plaintext messages.

Encryption and decryption happen exclusively on client devices.

The server must never store:

plaintext messages

decrypted attachments

private encryption keys

conversation encryption keys

message-search indexes containing plaintext

plaintext encrypted-message backups

The server may store metadata required for routing, such as:

message ID

conversation ID

sender device ID

recipient conversation

ciphertext

protocol version

encrypted-message expiration

delivery status

Account authentication

Prefer WebAuthn/passkeys.

Also optionally support:

email + password

If passwords are supported:

hash passwords using Argon2id

never store passwords

use unique salts

apply login rate limiting

support optional TOTP 2FA

Account authentication must remain separate from messaging encryption.

Logging into an account must NOT reveal existing encryption keys to the server.

User IDs

Never use @username as the permanent identity.

Example:

user_id:
UUIDv7 or random UUID

username:
alex

display:
@alex

Users may eventually change usernames while their internal identity remains unchanged.

Device model

Every device receives its own:

device_id
identity key pair
cryptographic state
created timestamp
last-seen timestamp

Example:

@alex
├── Desktop
├── Laptop
└── Phone

Never use one private identity key shared across all devices.

Private keys must be generated on-device.

Private keys never leave the device unencrypted.

Local storage

Store message history locally.

For web clients use:

IndexedDB

For native/desktop clients use something similar to:

SQLite

The local database itself should be encrypted where practical.

Store:

decrypted message history

conversation information

cryptographic state

attachment references

reactions

reply relationships

device state

Encryption keys should preferably use platform-protected storage:

Windows:
DPAPI / Credential Manager

macOS:
Keychain

Linux:
Secret Service / keyring

Mobile:
Keychain / Android Keystore

Browser:
use WebCrypto and protect persisted key material as strongly as the browser platform permits.

Encryption protocol

DO NOT invent a custom encryption algorithm.

Do not use:

ICE
raw PGP
age chained with another cipher
home-built AES protocols

For direct messages use a proven ratcheting messaging protocol.

Recommended design:

X25519 identity/key agreement
+
HKDF-SHA-256
+
Double Ratchet
+
ChaCha20-Poly1305

or use a thoroughly audited implementation of an established secure messaging protocol instead of implementing these primitives manually.

Every message should use fresh derived key material.

Old message keys should be erased after use where feasible.

Never reuse nonces.

Use cryptographically secure operating-system randomness.

Group encryption

Groups have a maximum of 10 users.

Use Messaging Layer Security (MLS) or another well-reviewed group messaging construction providing:

forward secrecy

membership authentication

key rotation

secure member addition

secure member removal

epoch changes

When someone leaves or is removed:

OLD GROUP

Alice
Bob
Charlie
Dave

REMOVE DAVE

↓

create new cryptographic epoch

↓

Alice
Bob
Charlie

↓

new cryptographic state

Dave must not be able to decrypt messages from the new epoch.

When adding a user, do not automatically provide previous plaintext history unless the group deliberately shares encrypted history.

Message format

The SERVER may receive something resembling:

{
"message_id": "uuid",
"conversation_id": "uuid",
"sender_device_id": "uuid",
"protocol_version": 1,
"epoch": 23,
"ciphertext": "...",
"created_at": "...",
"expires_at": "..."
}

The encrypted payload may contain:

{
"type": "message",
"text": "hello",
"reply_to": "...",
"mentions": ["user_uuid"],
"attachments": [],
"reactions": [],
"client_timestamp": 123456789
}

Prefer encrypting as much message metadata as practical.

Local-first message model

Normal message lifecycle:

Alice writes:
hello Bob

↓

Alice client encrypts message

↓

ciphertext sent to VPS

↓

VPS queues ciphertext

↓

Bob's device receives ciphertext

↓

Bob's device acknowledges delivery

↓

Bob decrypts locally

↓

Bob stores plaintext locally

↓

server deletes queued ciphertext according to retention rules

The VPS should not act as permanent message-history storage.

Offline users

Because users may be offline, the VPS may temporarily store encrypted queued messages.

Example retention:

pending ciphertext:
maximum 7–30 days

delivered ciphertext:
delete shortly after all intended devices acknowledge receipt

Make this configurable.

Never delete a message before delivery unless expiration/disappearing-message rules require it.

Delivery states

Support:

queued
sent
delivered
read

Read receipts should be optional.

Send read receipts as encrypted conversation events when practical.

Synchronization

When a user owns multiple devices:

Alice Desktop
Alice Laptop
Alice Phone

messages should be encrypted/routed to authorized devices.

New devices must not silently become trusted.

Device onboarding flow:

login

↓

new device generates identity key

↓

existing trusted device receives approval request

↓

existing device verifies new device

↓

device becomes trusted

↓

conversation cryptographic membership updates

Display:

Trusted Devices

✓ Desktop

✓ iPhone

! Chrome — awaiting approval

New-device history

Since the server does not permanently hold plaintext history, a new device should not magically download plaintext conversations.

Options:

No historical messages on newly added devices.

or:

Device-to-device encrypted history transfer.

Preferred:

new device displays QR code

↓

existing trusted device scans it

↓

authenticated encrypted channel created

↓

selected local history transferred directly or via end-to-end encrypted temporary blobs

↓

temporary server blobs deleted

The server must never receive the history decryption key.

Key verification

Provide identity verification.

Support:

QR verification

and/or

human-readable safety numbers

Example:

@bob

48291 19382 58301
84029 19384 10293

Warn users when another user's identity/device keys unexpectedly change.

Consider adding key transparency later.

@username directory

Endpoint concept:

GET /users/

Response:

{
"user_id": "...",
"username": "bob",
"display_name": "Bob",
"avatar_url": "...",
"devices": [
{
"device_id": "...",
"identity_public_key": "...",
"key_package": "..."
}
]
}

Never return private keys.

Use immutable user IDs internally.

Conversations

Types:

DM
GROUP

Database example:

conversations

id

type

creator_user_id

created_at

conversation_members

conversation_id

user_id

role

joined_at

removed_at

Do not store plaintext group names if maximum privacy is desired.

Instead store encrypted conversation metadata.

PostgreSQL schema

users

id

username

display_name

avatar_storage_id

password_hash nullable

created_at

devices

id

user_id

identity_public_key

device_name

created_at

last_seen

revoked_at

key_packages

id

device_id

public_key_package

consumed

created_at

conversations

id

type

creator_user_id

created_at

conversation_members

conversation_id

user_id

role

joined_at

removed_at

message_queue

id

conversation_id

sender_device_id

ciphertext

protocol_version

epoch

created_at

expires_at

message_deliveries

message_id

recipient_device_id

delivered_at

acknowledged_at

attachments

id

uploader_user_id

object_storage_key

encrypted_size

created_at

expires_at

sessions

id

user_id

device_id

hashed_session_token

expires_at

blocks

blocking_user_id

blocked_user_id

reports

id

reporter_user_id

reported_user_id

submitted_content

created_at

Never add:

message_plaintext
attachment_decryption_key
private_identity_key

to the server database.

Attachments

Attachments must be encrypted client-side before uploading.

Process:

select photo.jpg

↓

generate random 256-bit attachment encryption key

↓

encrypt file locally using authenticated encryption

↓

upload ciphertext

↓

server stores random object ID

↓

attachment key and file metadata included inside encrypted message

↓

recipient downloads encrypted blob

↓

recipient decrypts locally

The server sees encrypted bytes only.

Do not use original filenames as object-storage keys.

Use random object IDs.

Example:

attachments/
9a8c4341-5182-4c73.bin

not:

attachments/
alex-secret-photo.jpg

Validate size before upload.

Limit allowed file sizes.

Serve encrypted attachments from a separate storage domain where practical.

Do not execute uploaded content.

WebSocket events

Possible server routing events:

message.enqueue
message.delivery
message.ack
presence.update
typing.start
typing.stop
conversation.member_added
conversation.member_removed
device.added
device.revoked

Actual message bodies must remain encrypted.

Typing/presence are metadata and users should have privacy controls for them.

Presence

Support:

online
idle
offline

Do not expose exact last-seen timestamps unless users explicitly enable them.

Presence can be ephemeral using Redis.

Do not permanently store unnecessary presence logs.

Discord-style UI

Desktop layout:

LEFT SIDEBAR

Home
DM list
Group list

CENTER

conversation title
message history
message composer

RIGHT / MODAL

members
device information
conversation settings

Support:

@mentions
replies
reactions
drag-and-drop attachments
typing indicators
unread badges
online indicators
message edit/delete
optional disappearing messages

Message deletion

Local-first means deletion semantics must be explicit.

"Delete for me":
remove local copy only

"Request delete for everyone":
send authenticated encrypted deletion event to members

Clients receiving the event remove their local copies.

Do not claim this guarantees deletion because another participant may already have copied, exported, screenshotted, modified, or backed up the content.

Editing messages

Edits should be new authenticated encrypted events.

Example:

{
"type": "message_edit",
"target_message_id": "...",
"new_text": "corrected message"
}

Never mutate cryptographic history silently.

Reactions

Reactions should preferably be encrypted events:

{
"type": "reaction_add",
"target": "...",
"emoji": "👍"
}

Blocking

If Alice blocks Bob:

prevent Bob from creating new direct conversations

stop message routing where appropriate

hide presence

prevent calls/friend requests if those features exist

Blocking is enforced by both server authorization and client UI.

Reporting

Because the server cannot decrypt normal messages, moderation must use explicit user reports.

When user selects:

Report Message

show:

"Include this message and selected surrounding context in the report?"

If user consents:

client sends the selected decrypted content to the moderation endpoint.

Only deliberately reported content becomes visible to moderators.

Clearly explain this behavior.

Server authorization

Never trust IDs sent by clients.

For every action verify:

authenticated session

device belongs to authenticated user

user belongs to conversation

sender has permission

group member count <= 10

referenced objects exist

upload authorization is valid

Prevent IDOR vulnerabilities.

API protections

Implement:

rate limiting
request-size limits
schema validation
CSRF protection where relevant
strict CORS
secure cookies
short-lived access sessions
refresh-token rotation if tokens are used
authentication attempt throttling
username enumeration protections where reasonable

Browser security

This is extremely important because XSS can steal plaintext before encryption.

Use a strict Content Security Policy.

Avoid:

unsafe-inline
unsafe-eval

Sanitize all user-generated rendering.

Never inject message HTML directly.

Render message content as text or through a carefully sanitized Markdown renderer.

Use:

HttpOnly
Secure
SameSite

cookies where appropriate.

Apply:

X-Content-Type-Options
Referrer-Policy
Permissions-Policy
frame-ancestors

and other modern security headers.

VPS deployment

Suggested stack:

Ubuntu LTS
Docker
Docker Compose
Caddy
Application server
PostgreSQL
Redis
MinIO or external S3-compatible storage

Expose publicly:

80 -> redirect to HTTPS
443 -> Caddy HTTPS

Do NOT expose publicly:

PostgreSQL
Redis
MinIO admin API

Bind internal services to private Docker networking or localhost.

Firewall

Allow only required ports:

22 SSH
80 HTTP
443 HTTPS

Ideally restrict SSH access further.

Use SSH keys only.

Disable password SSH login.

Disable root SSH login after initial provisioning.

Use sudo through an administrative account.

VPS hardening

Configure:

automatic security updates
fail2ban or equivalent
firewall
SSH keys
non-root application containers
least-privilege file permissions
regular security patches
container image pinning
dependency scanning
audit logging

Do not store secrets inside the Git repository.

Use environment variables or a secret manager.

Backups

Back up:

PostgreSQL account/conversation metadata
server configuration
encrypted attachment blobs if required

The backup must still not contain plaintext messages or private encryption keys.

Encrypt server backups independently.

Test restoration.

Logging

NEVER log:

plaintext messages
private keys
encryption keys
full authentication tokens
passwords
attachment keys
decrypted payloads

Log:

request ID
server error category
endpoint
HTTP status
performance information
security events

Be careful with request-body logging.

Disable it for encrypted-message/authentication endpoints.

Open-source repository

Recommended structure:

/
├── apps/
│   ├── web/
│   └── server/
│
├── packages/
│   ├── crypto/
│   ├── protocol/
│   ├── types/
│   └── ui/
│
├── infrastructure/
│   ├── docker/
│   ├── caddy/
│   └── compose.yaml
│
├── migrations/
├── docs/
│   ├── architecture.md
│   ├── cryptography.md
│   ├── threat-model.md
│   ├── privacy.md
│   └── contributing.md
│
├── SECURITY.md
├── CONTRIBUTING.md
├── LICENSE
└── README.md

Crypto package rule

Keep cryptographic protocol code isolated.

packages/crypto/

must expose high-level functions such as:

createDeviceIdentity()
createConversation()
encryptMessage()
decryptMessage()
addGroupMember()
removeGroupMember()
rotateKeys()
verifyIdentity()
encryptAttachment()
decryptAttachment()

Application code should not manually manipulate raw keys, nonces, or primitive cipher operations.

Use an established cryptographic library.

Threat model

Document protection against:

database theft
VPS compromise
passive network interception
malicious users
stolen encrypted server backups
removed group members
replayed messages
message modification

Clearly state limitations against:

compromised recipient devices
malware
screenshots
users intentionally forwarding messages
browser extensions reading page content
a fully compromised client build
traffic-analysis metadata
endpoint compromise

Replay protection

Every encrypted message/event should have:

unique message ID
conversation identity
sender identity
sequence/ratchet state

Clients must detect duplicate/replayed messages.

Protocol versions

Every encrypted envelope must contain a protocol version.

Example:

{
"protocol_version": 1,
"conversation_id": "...",
"ciphertext": "..."
}

Never assume the encryption format will remain unchanged forever.

Create a migration/versioning strategy.

Cryptographic agility

Support changing approved algorithms in later protocol versions.

Do not expose arbitrary algorithm selection to end users.

Prevent downgrade attacks.

Clients must reject obsolete/unsafe protocol versions after migration deadlines.

Privacy

Collect the minimum metadata needed to operate the service.

Avoid storing:

historical IP logs indefinitely
typing history
presence history
search history
unnecessary user-agent logs

Provide a clear privacy policy describing which metadata the server can see.

Do not claim "zero knowledge" unless the architecture genuinely satisfies the specific claim.

Core server principle

Treat the VPS as potentially breachable.

A database dump should reveal:

accounts
public cryptographic keys
conversation membership metadata
temporary ciphertext
encrypted attachments

but should NOT reveal normal message text.

Development priority

Build in this order:

account creation

passkey/login

device identity generation

@username search

encrypted DM creation

encrypted message transport

local message database

offline ciphertext queue

delivery acknowledgements

encrypted groups

member add/remove key rotation

encrypted attachments

trusted-device management

device-to-device history transfer

blocking/reporting

disappearing messages

key verification

security audit/hardening

Do not add unnecessary features before encryption/session management is reliable.

Absolute requirements

DO:

generate private keys client-side

use established crypto libraries

encrypt before network transmission

authenticate ciphertext

rotate cryptographic state

encrypt attachments locally

make devices independently identifiable

revoke lost devices

delete delivered server ciphertext

use TLS in addition to E2EE

document the threat model

publish protocol documentation with the open-source repository

DO NOT:

invent encryption algorithms

use ICE

chain PGP + age + other encryption

reuse nonces

hardcode keys

store private keys on VPS

store plaintext messages on VPS

log plaintext messages

allow silent device additions

blindly trust usernames as identities

expose PostgreSQL/Redis publicly

claim deleted messages cannot exist elsewhere

The end result should feel approximately like a lightweight private Discord:

@username accounts
fast real-time DMs
groups of <=10
replies
reactions
attachments
presence
typing indicators

while cryptographically behaving more like a modern end-to-end encrypted messenger.

The central design philosophy is:

THE CLIENT OWNS THE MESSAGE.

THE VPS ONLY ROUTES CIPHERTEXT.

THE USER'S DEVICE HOLDS THE READABLE HISTORY.