- 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
8.0 KiB
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
- User registers/logs in ONCE on home server
- Gets JWT token + encrypted keypair
- To join other servers: sign a message with private key
- Other server verifies signature against home server's public key
- 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)
- Sticker Packs - Telegram-style, live on community servers, clients cache
- Home Server Migration - Sign migration certificate, chain of trust
- File Uploads - With cross-server URLs
- Message Forwarding - Embed with link to original
- Permissions - Role-based access to channels
- 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
- Implement HTTP client in
fetchPublicKeyFromHomeServer() - Add WebSocket for real-time
- Add profile sync (client pushes to all servers on change)
- Test cross-server join flow
- Add proper error handling and logging