Revised ARCHITECTURE.md based on CLIENT_INTEGRATION.md, to match the

current implementation
This commit is contained in:
Foxyz committed 2026-02-20 16:37:30 +03:00
1 parent 2222e2af4a
commit 458be6f991
3 files changed
+121 -341

No files matched your search

+6
View File
@@ -21,3 +21,9 @@ build/
# OS # OS
.DS_Store .DS_Store
Thumbs.db Thumbs.db
# Conversation exports (local only)
conversation_export/
# AI
.claude
+115 -98
View File
@@ -1,155 +1,172 @@
# Reliquary Server Architecture - Complete Reference # Reliquary Architecture & Protocol Reference
## 1. Project Overview & The Reliquary Protocol ## 1. Project Overview
Reliquary is a self-hosted, highly optimized Discord alternative for private communities. It operates on the **Reliquary Protocol**, a decentralized identity and routing model. **Reliquary** is a self-hosted, high-performance chat platform operating on the **Reliquary Protocol**.
* **Philosophy:** Desktop-first, native performance, decentralized identity.
* **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. * **Routing:** No server-to-server federation. The Client handles all routing.
* **No Server-to-Server Federation:** Servers don't talk to each other. The client handles all cross-server fetching and syncing. * **Identity:** Cryptographic keypairs (secp256k1). User ID: `user@home.server`.
* **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. * **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 ### ✅ Core Features
* **Identity System:** secp256k1 keypair/signatures via `crypton` EC (Targeting Ed25519 migration later). * **Auth:** JWT-based. Register/Login returns encrypted keypair.
* **Auth Flow:** Single Sign-On on Home Server (returns JWT + encrypted keypair). * **Join Flow:** Cryptographic signature verification.
* **Cross-Server Join Flow:** Signature-based verification. Guest servers fetch public keys from Home Servers via HTTP. * **REST API:** Basic CRUD for Channels and Text-only Messages.
* **Middleware:** JWT authentication middleware. * **Gateway:** WebSocket server for event fanout (Specification defined in Section 4).
* **Basic CRUD:** Channels and Messages (Text only) with author caching.
### 💾 Current Database Models (ObjectBox) ### 💾 Database Models (ObjectBox)
* **User:** `id`, `userId` ("alice@home.com"), `publicKey`, `passwordHash` (home only), etc. * **User:** `userId`, `publicKey`, `passwordHash` (home only).
* **Channel:** `id`, `name`, `createdAt`. * **Channel:** `id`, `name`, `createdAt`.
* **Message:** `id`, `channelId`, `authorLocalId`, `authorUserId`, `content` (currently simple text), `createdAt`. * **Message:** `id`, `channelId`, `authorUserId`, `content`, `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`
--- ---
## 3. Active Implementation Target: The Component & File System ## 3. Active Implementation Target: Component & File System
*⚠️ THIS IS THE IMMEDIATE NEXT STEP TO BE CODED. ⚠️* *⚠️ 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) ### 3.1 UX Flow: Staged Uploads (Discord-Style)
To preserve server storage and bandwidth while maintaining a fast, non-blocking UI, Reliquary uses a **Staged Upload Pipeline**. 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.*
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 (Client):** User hits "Send". Client adds a "Dummy Message" to the UI with a progress bar.
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):** Client POSTs files to `/api/uploads`.
3. **Upload (Network Action):** The client initiates `POST /api/uploads` for the files. 4. **Publish (Network):** Client gets `FileId`s, builds the `componentsJson`, and POSTs to `/api/messages`.
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 (Network):** Server broadcasts the new message via Gateway. Client swaps "Dummy" for "Real".
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.2 New/Modified Database Models
```dart ```dart
@Entity() @Entity()
class Message { class Message {
int id; // ... existing fields ...
int channelId;
// 1. Search & Notifications (Server Generated) // Search Index (Server concatenates text from all components)
// Server concatenates all text from components upon ingestion.
String content; String content;
// 2. The Component Tree (Source of Truth for UI) // Source of Truth (JSON List<Component>)
// JSON string representing List<Component>
String componentsJson; String componentsJson;
// ... existing author and date fields ...
} }
@Entity() @Entity()
class File { class File {
int id; int id;
int uploaderId; int uploaderId;
String path; // Relative storage path (/uploads/...) String path; // /uploads/uuid.jpg
String mimeType; String mimeType;
int width; // CRITICAL for layout
int height;
int sizeBytes; int sizeBytes;
// Safety Net Garbage Collection bool isAttached; // False until linked to a Message
// 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
DateTime createdAt; DateTime createdAt;
} }
``` ```
### 3.3 The Reliquary Component Schema (JSON) ### 3.3 Component JSON Schema
All components stored in `componentsJson` must follow this base schema to prevent client crashes and layout shifts. All items in `componentsJson` must follow this structure.
**Base Schema:** **Base Wrapper:**
```json ```json
{ {
"type": "string", // "text", "gallery", "code", "embed", "collapsible" "type": "text | gallery | collapsible | embed",
"modifiers":, // e.g., (client applies blur overlay) "modifiers":, // "spoiler" applies blur
"fallback": "string", // Plain text for older clients / notifications "fallback": "String for push notifs",
"data": { } // Type-specific payload "data": { ... }
} }
``` ```
**Type: Text** **Gallery Component (Images/Video):**
```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).*
```json ```json
{ {
"type": "gallery", "type": "gallery",
"modifiers": [], "modifiers": [],
"fallback": "", "fallback": "Image",
"data": { "data": {
"layout": "grid",
"items": "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 ```json
{ {
"type": "collapsible", "op": "event",
"modifiers": [], "event": "message.created",
"fallback": "", "channelId": 12,
"data": { "message": {
"summary": "Server Logs (Click to expand)", "id": 101,
"children":, "data": { "language": "bash", "content": "Error 500" } } "content": "Legacy text or fallback",
] "componentsJson": "[...]",
"authorId": "alice@home",
"createdAt": "..."
} }
} }
``` ```
*(Also supports `message.updated` and `message.deleted`)*
### 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`.
--- ---
## 4. Future Roadmap (Do NOT implement yet) ## 5. API Reference: Pagination & Uploads
* **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. ### GET /api/messages
* **Garbage Collector:** A daily worker to purge unattached files from the disk. 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"
}
```
-243
View File
@@ -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.