docs: document S2S profile sync relay and write-protection guard

This commit is contained in:
Jannis Braun
2026-04-07 14:01:44 +02:00
parent a7cd6a1bf3
commit a023c85cad
2 changed files with 24 additions and 7 deletions
+2
View File
@@ -28,6 +28,8 @@ GET /users/:id → { user }
GET /users/:id/mutuals ?homeUserId= → { mutualFriends[], mutualSpaces[] }
```
**Write protection:** If the authenticated user is a replicated user (`homeInstance` is set), the following fields are rejected with 403: `displayName`, `avatar`, `banner`, `accentColor`, `avatarColor`, `bio`. These fields are managed by the home instance via S2S relay.
## Spaces (`routes/spaces.ts`) — auth required
```
GET /spaces → { spaces[] }
+22 -7
View File
@@ -706,15 +706,30 @@ When relay events carry `FederationRelayProfileSnapshot` data:
`hydrateReplicatedUserProfile` only fills null/empty fields. Avatar/banner are overwritten only if the current value is not an absolute URL (catches stale bare filenames).
### Client-side LWW Sync (`profileSync.ts`)
#### Profile Sync (S2S)
Operates via the web client, not S2S relay:
- On connect to a remote instance, compares `profileUpdatedAt` timestamps
- If home is newer: pushes profile to remote (re-uploads avatar/banner)
- If remote is newer: pulls from remote to home, then relays to all other remotes
- Incremental: `syncProfileUpdateToRemotes` pushes partial updates after local profile edits
Profile data is synced server-to-server. The home instance is authoritative — when a user updates their profile, the home server queues a `profile_update` relay event to all active peers via the outbox.
This is a **client-driven** mechanism -- it only runs when a user is actively connected to multiple instances. It does not use the relay pipeline or outbox.
**Event:** `profile_update` (contextType: `profile`)
**Payload:** `FederationProfileUpdatePayload` — full snapshot of 6 durable fields:
- `homeUserId`, `homeInstance`, `profileUpdatedAt` (monotonic version)
- `displayName`, `avatar` (absolute URL or null), `banner` (absolute URL or null)
- `accentColor`, `avatarColor`, `bio`
**Targeting:** Broadcast to all active peers. Peers silently accept if they have no replica.
**Coalescing:** `entityId = homeUserId`. Rapid successive edits coalesce to one delivery per peer.
**Processing:** Remote overwrites all 6 fields unconditionally. Rejects if incoming `profileUpdatedAt ≤ stored`. Broadcasts `user_updated` to local WS clients.
**Write protection:** Remote instances reject PATCH /users/@me profile field updates for replicated users (homeInstance set). Profile data is read-only on remote — only updated via S2S relay.
**Bootstrap:** When a new origin appears in a user's `replicatedInstances`, the home server queues a targeted `profile_update` to that peer.
**File handling:** Avatar/banner stored as absolute URLs pointing to the home instance's `/api/uploads/`. Browser caching (immutable, 1-year) ensures performance.
**Replaces:** Client-driven `profileSync.ts` (deleted). `hydrateReplicatedUserProfile` still bootstraps null fields during DM/friend relay — it is not replaced.
---