Files
chat-server/ARCHITECTURE.md
T

9.8 KiB

Chat Server Architecture - Complete Reference

Project Overview

Self-hosted Discord alternative with decentralized identity. Each user has a "home server" where their identity lives, and can join other "guest servers" seamlessly using cryptographic signatures.

Core Architecture Decisions

Home Server Model

  • NO identity server - just use one regular server as "home"
  • NO federation protocol - servers don't talk to each other
  • Client does everything - syncs, cross-server requests, profile updates

Identity System

  • Current implementation: secp256k1 keypair/signatures via crypton EC
  • Target (planned): Ed25519 migration
  • Private key = proof of identity
  • Public key stored on home server at /.well-known/users/:username
  • userId format: username@serverhost

Single Sign-On Flow

  1. User registers/logs in ONCE on home server
  2. Gets JWT token + encrypted keypair
  3. To join other servers: sign a message with private key
  4. Other server verifies signature against home server's public key
  5. No password ever sent to guest servers

What's Implemented

✅ Done

  • User model with keypair, home server reference
  • Channel and Message models
  • Crypto service (keypair generation, signing, verification)
  • JWT authentication middleware
  • Register handler (creates keypair)
  • Login handler (returns encrypted keypair for device sync)
  • Join server handler (signature-based verification with identity checks)
  • HTTP client for cross-server public key fetching
  • Public endpoints for cross-server identity verification
  • Channel CRUD
  • Message CRUD with author caching

❌ Not Implemented

  • WebSocket gateway for real-time events
  • Notification system (mentions/replies/unread state)
  • Push delivery pipeline (FCM/APNs/WebPush device tokens + dispatch)
  • Profile sync (push updates to all servers)
  • Sticker packs (future feature)
  • File uploads
  • Permissions system (roles exist but not enforced)
  • Message editing/deletion permissions

Database Models

// User - works for both HOME and GUEST servers
@Entity()
class User {
  int id;
  String userId;           // "alice@homeserver.com"
  String username;
  String? displayName, avatarUrl, bio;
  
  String publicKey;        // Ed25519, always stored
  
  // HOME SERVER ONLY:
  String? passwordHash;
  String? encryptedPrivateKey;  // For device sync
  String? keyPairSalt;
  
  bool isLocal;            // true = this is home server
  String? homeServer;      // if guest, where is home?
  List<String> roles;      // Server-specific
  DateTime createdAt, lastSyncedAt;
}

@Entity()
class Channel {
  int id;
  String name;
  String? description;
  DateTime createdAt;
}

@Entity()
class Message {
  int id;
  int channelId;
  
  // Author info (includes cross-server fetch capability)
  int authorLocalId;
  String authorUserId;       // "alice@home.server"
  String? authorHomeServer;
  
  // Cached snapshot at message time
  String authorUsername;
  String? authorAvatarUrl;
  
  String content;
  DateTime createdAt, editedAt;
}

@Entity()
class Notification {
  int id;
  int recipientLocalId;
  String recipientUserId;    // "alice@home.server"

  String type;               // "mention", "reply", "system"
  String title;
  String body;

  int? channelId;
  int? messageId;

  bool isRead;
  DateTime createdAt;
  DateTime? readAt;
}

API Endpoints

Public (No Auth)

POST /auth/register     → Create account on HOME server (generates keypair)
POST /auth/login        → Login to HOME server (returns keypair backup)
POST /auth/join         → Join THIS server with signature (no password)

GET /.well-known/users/:username   → Public profile + public key
GET /.well-known/verify/:userId    → Verify user exists
GET /.well-known/server            → Server info

Protected (JWT Required)

GET  /api/me                      → Current user profile

GET  /api/channels                → List channels
POST /api/channels                → Create channel
GET  /api/channels/:id            → Get channel

GET  /api/messages?channelId=X    → Get messages
POST /api/messages                → Send message
PUT  /api/messages/:id            → Edit message
DELETE /api/messages/:id          → Delete message

GET  /api/notifications           → List notifications
POST /api/notifications/read      → Mark notifications read

POST /api/push/devices            → Register device push token
DELETE /api/push/devices/:id      → Remove push token

Realtime & Notifications (Planned MVP)

  • Add WebSocket endpoint (e.g. /gateway) authenticated by JWT.
  • Keep REST as source of truth; WebSocket is for event fanout.
  • On message create/edit/delete:
    1. Write to DB
    2. Emit gateway event to subscribed clients
    3. Generate notification records for mention/reply targets
  • If recipient is online, deliver notification over gateway immediately.
  • If recipient is offline and has device token, enqueue push dispatch.
  • Reconnect strategy: client backfills missed messages using GET /api/messages?channelId=X&after=lastSeenMessageId.
  • Full Discord-style resume/replay is optional and can be added later.

Key Files

lib/
├── server.dart              # Entry point, routes, ObjectBox init
├── models.dart              # User, Channel, Message entities
├── objectbox.g.dart         # Generated
│
├── config/config.dart       # Server host, JWT secret, port
│
└── src/
    ├── auth/
    │   ├── crypto_service.dart   # Keypair, signing, encryption
    │   └── auth_middleware.dart  # JWT verification
    │
    ├── handlers/
    │   ├── auth_handlers.dart    # register, login, join, me
    │   ├── public_handlers.dart  # .well-known/* 
    │   ├── channel_handlers.dart
    │   └── message_handlers.dart
    │
    └── utils/extensions.dart     # Request/Response helpers

Join Server Flow (Implemented)

CLIENT                              GUEST SERVER              HOME SERVER
  │                                      │                         │
  │ POST /auth/join                      │                         │
  │ {userId, homeServer, timestamp,      │                         │
  │  signature}                          │                         │
  │─────────────────────────────────────►│                         │
  │                                      │                         │
  │                                      │ GET /.well-known/users/ │
  │                                      │ alice                   │
  │                                      │────────────────────────►│
  │                                      │                         │
  │                                      │ {publicKey, ...}        │
  │                                      │◄────────────────────────│
  │                                      │                         │
  │                                      │ verify signature         │
  │                                      │ create shadow account    │
  │                                      │                         │
  │ {token, localId}                     │                         │
  │◄─────────────────────────────────────│                         │

The join flow now:

  • Fetches identity from home server over HTTP (/.well-known/users/:username)
  • Verifies claimed userId matches home-server response
  • Verifies signature before issuing token (including already-joined users)

Tech Stack

  • Framework: Relic 1.0 (Dart web server)
  • Database: ObjectBox
  • Auth: JWT + secp256k1 signatures (current), Ed25519 (planned)
  • Crypto: crypton package (EC)
  • Password: bcrypt

Design Decisions Rationale

Decision Why
No identity server Avoids hosting another service, one server = home
No server-to-server Eliminates federation complexity, client handles everything
Keypair identity Allows passwordless joins, proves identity cryptographically
Message author caching Messages readable even if home server is down
userId format user@server Simple, familiar, self-documenting

Constraints

  • User CANNOT quit home server (that's where identity lives)
  • User CANNOT change home server easily (migration = future feature)
  • Home server MUST be online to join NEW servers
  • Home server offline = can still use already-joined servers (cached keys)

Future Features (Out of Scope for MVP)

  1. Sticker Packs - Telegram-style, live on community servers, clients cache
  2. Home Server Migration - Sign migration certificate, chain of trust
  3. File Uploads - With cross-server URLs
  4. Message Forwarding - Embed with link to original
  5. Permissions - Role-based access to channels
  6. WebSocket - Real-time message delivery
  7. Advanced Notification Rules - Mute/priority/snooze/digests

Config to Change

// lib/config/config.dart
static const String serverHost = 'your-actual-domain.com';  // CHANGE THIS
static const String jwtSecret = 'use-env-variable';         // CHANGE THIS

Next Steps

  1. Add WebSocket gateway (/gateway) with JWT auth + channel subscriptions
  2. Add message history cursor pagination (before + after)
  3. Implement notifications (DB model + generation rules + unread counters)
  4. Add push token registration + push dispatch worker
  5. Add profile sync (client pushes to all servers on change)
  6. Add proper error handling, replay protection, and logging