Files
chat-server/ARCHITECTURE.md
Z User 6a8977c4c3 feat: Initial MVP with home server identity model
- User model with Ed25519 keypair identity
- Register/login on home server (creates keypair)
- Join guest servers with signature verification
- Public endpoints for cross-server identity
- Channel and message CRUD
- JWT authentication middleware

Architecture: Home server model, no federation, client-driven sync
2026-02-19 23:29:52 +00:00

8.0 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

  • Ed25519 keypair generated on registration
  • 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 - stub)
  • Public endpoints for cross-server identity verification
  • Channel CRUD
  • Message CRUD with author caching

❌ Not Implemented

  • HTTP client for cross-server public key fetching
  • WebSocket for real-time messages
  • 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;
}

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

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 (Needs Implementation)

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 fetchPublicKeyFromHomeServer() function in auth_handlers.dart needs actual HTTP implementation.


Tech Stack

  • Framework: Relic 1.0 (Dart web server)
  • Database: ObjectBox
  • Auth: JWT + Ed25519 signatures
  • Crypto: crypton package for Ed25519
  • 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

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. Implement HTTP client in fetchPublicKeyFromHomeServer()
  2. Add WebSocket for real-time
  3. Add profile sync (client pushes to all servers on change)
  4. Test cross-server join flow
  5. Add proper error handling and logging