Revised ARCHITECTURE.md based on CLIENT_INTEGRATION.md, to match the
current implementation
This commit is contained in:
1 parent
2222e2af4a
commit
458be6f991
3 files changed
+124
-344
No files matched your search
@@ -21,3 +21,9 @@ build/
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Conversation exports (local only)
|
||||
conversation_export/
|
||||
|
||||
# AI
|
||||
.claude
|
||||
+118
-101
@@ -1,155 +1,172 @@
|
||||
# Reliquary Server Architecture - Complete Reference
|
||||
# Reliquary Architecture & Protocol Reference
|
||||
|
||||
## 1. Project Overview & The Reliquary Protocol
|
||||
Reliquary is a self-hosted, highly optimized Discord alternative for private communities. It operates on the **Reliquary Protocol**, a decentralized identity and routing model.
|
||||
|
||||
* **Desktop-First & Native Performance:** The primary target is Desktop and Web, followed by Android (using foreground services for background connectivity). iOS and centralized Push Notifications are explicitly out of scope for the MVP to avoid centralized infrastructure.
|
||||
* **No Server-to-Server Federation:** Servers don't talk to each other. The client handles all cross-server fetching and syncing.
|
||||
* **Cryptographic Identity:** Users have a "Home Server" storing their public key (`/.well-known/users/:username`). They join "Guest Servers" using cryptographic signatures. No passwords are ever sent to Guest Servers.
|
||||
## 1. Project Overview
|
||||
**Reliquary** is a self-hosted, high-performance chat platform operating on the **Reliquary Protocol**.
|
||||
* **Philosophy:** Desktop-first, native performance, decentralized identity.
|
||||
* **Routing:** No server-to-server federation. The Client handles all routing.
|
||||
* **Identity:** Cryptographic keypairs (secp256k1). User ID: `user@home.server`.
|
||||
* **Push Notifications:** Explicitly out of scope for MVP. Desktop relies on background WebSockets; Android relies on Foreground Services.
|
||||
|
||||
---
|
||||
|
||||
## 2. Current State (What is already implemented)
|
||||
## 2. Current System State
|
||||
*The following features are implemented or have a fixed specification.*
|
||||
|
||||
### ✅ Implemented Features
|
||||
* **Identity System:** secp256k1 keypair/signatures via `crypton` EC (Targeting Ed25519 migration later).
|
||||
* **Auth Flow:** Single Sign-On on Home Server (returns JWT + encrypted keypair).
|
||||
* **Cross-Server Join Flow:** Signature-based verification. Guest servers fetch public keys from Home Servers via HTTP.
|
||||
* **Middleware:** JWT authentication middleware.
|
||||
* **Basic CRUD:** Channels and Messages (Text only) with author caching.
|
||||
### ✅ Core Features
|
||||
* **Auth:** JWT-based. Register/Login returns encrypted keypair.
|
||||
* **Join Flow:** Cryptographic signature verification.
|
||||
* **REST API:** Basic CRUD for Channels and Text-only Messages.
|
||||
* **Gateway:** WebSocket server for event fanout (Specification defined in Section 4).
|
||||
|
||||
### 💾 Current Database Models (ObjectBox)
|
||||
* **User:** `id`, `userId` ("alice@home.com"), `publicKey`, `passwordHash` (home only), etc.
|
||||
* **Channel:** `id`, `name`, `createdAt`.
|
||||
* **Message:** `id`, `channelId`, `authorLocalId`, `authorUserId`, `content` (currently simple text), `createdAt`.
|
||||
|
||||
### 🌐 Current REST APIs
|
||||
* `POST /auth/register`, `POST /auth/login`, `POST /auth/join`
|
||||
* `GET /.well-known/users/:username`, `GET /.well-known/verify/:userId`
|
||||
* `GET /api/me`, `GET /api/channels`, `POST /api/channels`, `GET /api/channels/:id`
|
||||
* `GET /api/messages`, `POST /api/messages`, `PUT /api/messages/:id`, `DELETE /api/messages/:id`
|
||||
### 💾 Database Models (ObjectBox)
|
||||
* **User:** `userId`, `publicKey`, `passwordHash` (home only).
|
||||
* **Channel:** `id`, `name`, `createdAt`.
|
||||
* **Message:** `id`, `channelId`, `authorUserId`, `content`, `createdAt`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Active Implementation Target: The Component & File System
|
||||
*⚠️ THIS IS THE IMMEDIATE NEXT STEP TO BE CODED. ⚠️*
|
||||
## 3. Active Implementation Target: Component & File System
|
||||
*⚠️ THIS SECTION DESCRIBES NEW FEATURES TO BE CODED ⚠️*
|
||||
|
||||
We are migrating from simple text strings to a **Component-Based Message Structure** (Reliquary Components) to support rich media, link embeds, and collapsible UIs natively.
|
||||
We are migrating from plain text to **Reliquary Components**. This allows rich media, embeds, and collapsible UIs.
|
||||
|
||||
### 3.1 UX Flow: Staged Uploads & Dummy Messages (Discord-Style)
|
||||
To preserve server storage and bandwidth while maintaining a fast, non-blocking UI, Reliquary uses a **Staged Upload Pipeline**.
|
||||
|
||||
1. **Stage (Local Client Action):** User selects multiple files. The client displays local thumbnails in the input area. The user can drag to reorder, toggle `spoiler` modifiers on individual files or the whole gallery, and add text. *No network requests are made during this phase.*
|
||||
2. **Commit (Local Client Action):** The user hits "Send". The client clears the input box so the user can keep typing, and inserts a "Dummy Message" into the local chat history. This dummy displays a file upload progress bar.
|
||||
3. **Upload (Network Action):** The client initiates `POST /api/uploads` for the files.
|
||||
4. **Publish (Network Action):** Once the server responds with the `FileId`s and dimensions, the client constructs the final `componentsJson` tree and calls `POST /api/messages`.
|
||||
5. **Resolve:** The server processes the message and broadcasts it back via WebSocket. The client silently swaps the "Dummy Message" with the finalized, permanent message.
|
||||
|
||||
|
||||
### 3.2 Updated Database Models (To be modified/created)
|
||||
### 3.1 UX Flow: Staged Uploads (Discord-Style)
|
||||
To ensure instant UI feedback and save server bandwidth:
|
||||
1. **Stage (Client):** User picks files. Client shows local previews. User reorders/adds spoilers. *No network request yet.*
|
||||
2. **Commit (Client):** User hits "Send". Client adds a "Dummy Message" to the UI with a progress bar.
|
||||
3. **Upload (Network):** Client POSTs files to `/api/uploads`.
|
||||
4. **Publish (Network):** Client gets `FileId`s, builds the `componentsJson`, and POSTs to `/api/messages`.
|
||||
5. **Resolve (Network):** Server broadcasts the new message via Gateway. Client swaps "Dummy" for "Real".
|
||||
|
||||
### 3.2 New/Modified Database Models
|
||||
```dart
|
||||
@Entity()
|
||||
class Message {
|
||||
int id;
|
||||
int channelId;
|
||||
|
||||
// 1. Search & Notifications (Server Generated)
|
||||
// Server concatenates all text from components upon ingestion.
|
||||
// ... existing fields ...
|
||||
|
||||
// Search Index (Server concatenates text from all components)
|
||||
String content;
|
||||
|
||||
// 2. The Component Tree (Source of Truth for UI)
|
||||
// JSON string representing List<Component>
|
||||
// Source of Truth (JSON List<Component>)
|
||||
String componentsJson;
|
||||
|
||||
// ... existing author and date fields ...
|
||||
}
|
||||
|
||||
@Entity()
|
||||
class File {
|
||||
int id;
|
||||
int uploaderId;
|
||||
String path; // Relative storage path (/uploads/...)
|
||||
int uploaderId;
|
||||
String path; // /uploads/uuid.jpg
|
||||
String mimeType;
|
||||
int width; // CRITICAL for layout
|
||||
int height;
|
||||
int sizeBytes;
|
||||
|
||||
// Safety Net Garbage Collection
|
||||
// Because uploads and message creation are two separate API calls,
|
||||
// if the client crashes in between step 3 and 4, the file is orphaned.
|
||||
// A simple daily worker will delete files where isAttached == false and age > 24h.
|
||||
bool isAttached; // False until used in a POST /api/messages
|
||||
bool isAttached; // False until linked to a Message
|
||||
DateTime createdAt;
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 The Reliquary Component Schema (JSON)
|
||||
All components stored in `componentsJson` must follow this base schema to prevent client crashes and layout shifts.
|
||||
### 3.3 Component JSON Schema
|
||||
All items in `componentsJson` must follow this structure.
|
||||
|
||||
**Base Schema:**
|
||||
**Base Wrapper:**
|
||||
```json
|
||||
{
|
||||
"type": "string", // "text", "gallery", "code", "embed", "collapsible"
|
||||
"modifiers":, // e.g., (client applies blur overlay)
|
||||
"fallback": "string", // Plain text for older clients / notifications
|
||||
"data": { } // Type-specific payload
|
||||
"type": "text | gallery | collapsible | embed",
|
||||
"modifiers":, // "spoiler" applies blur
|
||||
"fallback": "String for push notifs",
|
||||
"data": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
**Type: Text**
|
||||
```json
|
||||
{
|
||||
"type": "text",
|
||||
"modifiers":[],
|
||||
"fallback": "Hello world",
|
||||
"data": { "content": "Hello **world**", "format": "markdown" }
|
||||
}
|
||||
```
|
||||
|
||||
**Type: Gallery (Images/Video)**
|
||||
*Must include aspect ratio or width/height to prevent Cumulative Layout Shift (CLS).*
|
||||
**Gallery Component (Images/Video):**
|
||||
```json
|
||||
{
|
||||
"type": "gallery",
|
||||
"modifiers": [],
|
||||
"fallback": "",
|
||||
"fallback": "Image",
|
||||
"data": {
|
||||
"layout": "grid",
|
||||
"items":
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Type: Collapsible (Expandable Tile)**
|
||||
---
|
||||
|
||||
## 4. Gateway Protocol Specification (WebSockets)
|
||||
*The server implements a WebSocket Gateway at `/gateway`.*
|
||||
|
||||
### Connection & Auth
|
||||
* **URL:** `ws://host:port/gateway`
|
||||
* **Auth:** `?token=<jwt>` (Query param) or `Authorization: Bearer <jwt>` (Header).
|
||||
* **OpCode: Ready** (Server -> Client on connect):
|
||||
```json
|
||||
{ "op": "ready", "connectionId": "gw_1", "userId": "alice@home" }
|
||||
```
|
||||
|
||||
### Client -> Server Ops
|
||||
**Subscribe:**
|
||||
```json
|
||||
{ "op": "subscribe", "channelIds": [12, 13] }
|
||||
```
|
||||
**Unsubscribe:**
|
||||
```json
|
||||
{ "op": "unsubscribe", "channelIds": [12] }
|
||||
```
|
||||
**Heartbeat:**
|
||||
```json
|
||||
{ "op": "ping" }
|
||||
```
|
||||
|
||||
### Server -> Client Ops (Events)
|
||||
**Message Created:**
|
||||
```json
|
||||
{
|
||||
"type": "collapsible",
|
||||
"modifiers": [],
|
||||
"fallback": "",
|
||||
"data": {
|
||||
"summary": "Server Logs (Click to expand)",
|
||||
"children":, "data": { "language": "bash", "content": "Error 500" } }
|
||||
]
|
||||
"op": "event",
|
||||
"event": "message.created",
|
||||
"channelId": 12,
|
||||
"message": {
|
||||
"id": 101,
|
||||
"content": "Legacy text or fallback",
|
||||
"componentsJson": "[...]",
|
||||
"authorId": "alice@home",
|
||||
"createdAt": "..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 API Changes Required
|
||||
**1. New Endpoint: File Upload**
|
||||
* `POST /api/uploads` (multipart/form-data)
|
||||
* Returns: `{ "id": 101, "url": "/files/101.jpg", "width": 1920, "height": 1080 }`
|
||||
|
||||
**2. Modify Endpoint: Send Message**
|
||||
* `POST /api/messages`
|
||||
* Payload now expects: `{ "channelId": 1, "components": }`
|
||||
* Server logic required:
|
||||
1. Parse `components`.
|
||||
2. Extract all readable text to populate the `content` database column.
|
||||
3. Validate any `fileIds` inside galleries. Mark those `File` entities as `isAttached = true`.
|
||||
4. Save `Message` entity with `componentsJson`.
|
||||
*(Also supports `message.updated` and `message.deleted`)*
|
||||
|
||||
---
|
||||
|
||||
## 4. Future Roadmap (Do NOT implement yet)
|
||||
* **WebSocket Gateway:** Real-time event fanout (`/gateway`) with channel subscriptions.
|
||||
* **Embedder Background Worker:** A server-side queue that intercepts links in messages, fetches OpenGraph/yt-dlp data, and appends an `embed` component asynchronously.
|
||||
* **Garbage Collector:** A daily worker to purge unattached files from the disk.
|
||||
## 5. API Reference: Pagination & Uploads
|
||||
|
||||
### GET /api/messages
|
||||
Used for initial load and history backfilling.
|
||||
* **Params:**
|
||||
* `channelId` (required)
|
||||
* `limit` (default 50)
|
||||
* `before` (Message ID) -> Load older messages.
|
||||
* `after` (Message ID) -> Load newer messages (catch-up).
|
||||
|
||||
* **Response:**
|
||||
```json
|
||||
{
|
||||
"messages": [],
|
||||
"hasMore": true,
|
||||
"nextBefore": 51, // Cursor for older
|
||||
"nextAfter": 100 // Cursor for newer
|
||||
}
|
||||
```
|
||||
|
||||
### POST /api/uploads (New)
|
||||
* **Format:** `multipart/form-data`
|
||||
* **Response:**
|
||||
```json
|
||||
{
|
||||
"id": 505,
|
||||
"url": "/uploads/505.png",
|
||||
"width": 1920,
|
||||
"height": 1080,
|
||||
"mimeType": "image/png"
|
||||
}
|
||||
```
|
||||
@@ -1,243 +0,0 @@
|
||||
# Client Integration Guide
|
||||
|
||||
This document describes the current server contract you can rely on when implementing the client.
|
||||
|
||||
## Base Auth Model
|
||||
|
||||
- REST endpoints under `/api/*` require `Authorization: Bearer <jwt>`.
|
||||
- WebSocket gateway at `/gateway` also requires JWT.
|
||||
- Gateway token sources:
|
||||
1. `Authorization: Bearer <jwt>` (preferred when possible)
|
||||
2. query fallback: `?token=<jwt>` or `?access_token=<jwt>` (useful for browser WebSocket clients)
|
||||
|
||||
## Gateway (`/gateway`)
|
||||
|
||||
### Connect
|
||||
|
||||
- URL: `ws://<host>:<port>/gateway` (or `wss://` behind TLS)
|
||||
- On successful auth, server sends:
|
||||
|
||||
```json
|
||||
{
|
||||
"op": "ready",
|
||||
"connectionId": "gw_1",
|
||||
"userId": "alice@example.com",
|
||||
"username": "alice"
|
||||
}
|
||||
```
|
||||
|
||||
If auth fails, handshake is rejected with HTTP error (`401`/`403`) and no WebSocket session is established.
|
||||
|
||||
### Client -> Server Messages
|
||||
|
||||
All frames are JSON text.
|
||||
|
||||
#### Subscribe
|
||||
|
||||
```json
|
||||
{"op":"subscribe","channelId":12}
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```json
|
||||
{"op":"subscribe","channelIds":[12,13]}
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"op": "subscribed",
|
||||
"channelIds": [12, 13],
|
||||
"subscriptions": [12, 13]
|
||||
}
|
||||
```
|
||||
|
||||
If one or more channels do not exist:
|
||||
|
||||
```json
|
||||
{
|
||||
"op": "subscribed",
|
||||
"channelIds": [12],
|
||||
"subscriptions": [12],
|
||||
"unknownChannelIds": [999]
|
||||
}
|
||||
```
|
||||
|
||||
#### Unsubscribe
|
||||
|
||||
```json
|
||||
{"op":"unsubscribe","channelId":12}
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```json
|
||||
{"op":"unsubscribe","channelIds":[12,13]}
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"op": "unsubscribed",
|
||||
"channelIds": [12],
|
||||
"subscriptions": [13]
|
||||
}
|
||||
```
|
||||
|
||||
#### Heartbeat
|
||||
|
||||
Client may send:
|
||||
|
||||
```json
|
||||
{"op":"ping"}
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```json
|
||||
{"op":"heartbeat"}
|
||||
```
|
||||
|
||||
Server replies:
|
||||
|
||||
```json
|
||||
{
|
||||
"op": "pong",
|
||||
"ts": "2026-02-20T12:34:56.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
### Server -> Client Event Messages
|
||||
|
||||
After subscription, channel message mutations are pushed as:
|
||||
|
||||
#### Message Created
|
||||
|
||||
```json
|
||||
{
|
||||
"op": "event",
|
||||
"event": "message.created",
|
||||
"channelId": 12,
|
||||
"message": {
|
||||
"id": 101,
|
||||
"content": "hello",
|
||||
"authorId": "alice@example.com",
|
||||
"authorUsername": "alice",
|
||||
"authorAvatarUrl": null,
|
||||
"authorHomeServer": "https://home.example.com",
|
||||
"channelId": 12,
|
||||
"createdAt": "2026-02-20T12:34:56.000",
|
||||
"editedAt": null
|
||||
},
|
||||
"emittedAt": "2026-02-20T12:34:56.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
#### Message Updated
|
||||
|
||||
```json
|
||||
{
|
||||
"op": "event",
|
||||
"event": "message.updated",
|
||||
"channelId": 12,
|
||||
"message": { "...same message shape..." },
|
||||
"emittedAt": "2026-02-20T12:35:56.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
#### Message Deleted
|
||||
|
||||
```json
|
||||
{
|
||||
"op": "event",
|
||||
"event": "message.deleted",
|
||||
"channelId": 12,
|
||||
"messageId": 101,
|
||||
"emittedAt": "2026-02-20T12:36:56.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
### Gateway Error Payload
|
||||
|
||||
Malformed input or unsupported ops return:
|
||||
|
||||
```json
|
||||
{"op":"error","message":"..."}
|
||||
```
|
||||
|
||||
Common causes:
|
||||
- invalid JSON payload
|
||||
- missing/invalid `op`
|
||||
- unsupported op
|
||||
- missing `channelId`/`channelIds` for subscribe/unsubscribe
|
||||
|
||||
## Message History Pagination (`GET /api/messages`)
|
||||
|
||||
### Query Params
|
||||
|
||||
- `channelId` (required, int)
|
||||
- `limit` (optional, int, default `50`, max `100`)
|
||||
- `before` (optional, message ID cursor)
|
||||
- `after` (optional, message ID cursor)
|
||||
|
||||
Rules:
|
||||
- Do not send `before` and `after` together.
|
||||
- `before`/`after` must be positive integers.
|
||||
|
||||
### Response Shape
|
||||
|
||||
```json
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
"id": 100,
|
||||
"content": "text",
|
||||
"authorId": "alice@example.com",
|
||||
"authorUsername": "alice",
|
||||
"authorAvatarUrl": null,
|
||||
"authorHomeServer": "https://home.example.com",
|
||||
"channelId": 12,
|
||||
"createdAt": "2026-02-20T12:30:00.000",
|
||||
"editedAt": null
|
||||
}
|
||||
],
|
||||
"channelId": 12,
|
||||
"limit": 50,
|
||||
"hasMore": true,
|
||||
"nextBefore": 51,
|
||||
"nextAfter": 100
|
||||
}
|
||||
```
|
||||
|
||||
### Ordering Semantics
|
||||
|
||||
- no cursor: newest -> oldest (descending by message id)
|
||||
- `before=<id>`: older than cursor, still newest -> oldest
|
||||
- `after=<id>`: newer than cursor, oldest -> newest (ascending)
|
||||
|
||||
For client state:
|
||||
- `nextBefore`: cursor to request older page
|
||||
- `nextAfter`: cursor to request newer page
|
||||
|
||||
## Recommended Client Flow
|
||||
|
||||
1. Login/register/join and store JWT.
|
||||
2. Initial channel load: `GET /api/messages?channelId=<id>&limit=50`.
|
||||
3. Connect `/gateway` with token.
|
||||
4. Send `subscribe` for active channel(s).
|
||||
5. Merge realtime events into local message list:
|
||||
- `message.created`: insert if absent
|
||||
- `message.updated`: patch by id
|
||||
- `message.deleted`: remove/tombstone by id
|
||||
6. Pagination:
|
||||
- load older: call `before=<nextBefore>`
|
||||
- catch up newer (after reconnect): call `after=<lastSeenMessageId>`
|
||||
|
||||
## Practical Notes
|
||||
|
||||
- REST is source of truth; gateway is fanout only.
|
||||
- Gateway registry is currently in-memory on server instance (no cross-instance sync yet).
|
||||
- Channel permissions are not enforced yet by gateway subscriptions; only channel existence is checked.
|
||||
Reference in new issue
Block a user