diff --git a/docs/systems/api.md b/docs/systems/api.md index 4fd7d28e..8ee8f998 100644 --- a/docs/systems/api.md +++ b/docs/systems/api.md @@ -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[] } diff --git a/docs/systems/federation.md b/docs/systems/federation.md index d85ab68e..bb5ab751 100644 --- a/docs/systems/federation.md +++ b/docs/systems/federation.md @@ -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. ---