Single function builds complete relay payload with attachments (including
sourceUrl) and participants. Eliminates 4-way duplication that caused
the attachments: [] hardcode bug.
The relay was failing because processCreateEvent relied on the friends
table to discover the DM recipient, but friendships aren't federated
across instances. Also, resolveLocalUser matched deleted replicated
users before active ones.
- Add participants[] to FederationRelayEvent with homeUserId/homeInstance
for all DM channel members
- Add getDmParticipants() helper to look up member identities
- Include participants in outbox payloads (create/update) and sync events
- Rewrite processCreateEvent to resolve participants directly, compute
canonicalDmPairId, and findOrCreateDmChannel — removing the entire
friends-list fallback (60+ lines)
- Fix resolveLocalUser to filter out deleted users (is_deleted = 0)
and prefer the replicated user match when multiple candidates exist
The old query missed channels where both members appear as 'local'
(e.g., native Nova + native youruser on orbit). Using
canonical_pair_id is simpler and correct — any channel with a pair
ID is a federated DM that should be synced.
Messages by replicated users (e.g., Jannis browsing orbit) have
source_instance IS NULL because they were created on that instance.
The sync endpoint was incorrectly filtering these out, causing most
DM history to be missing from the sync response.
- buildRelayPayload now uses config.domain for local users instead of
empty string, so the relay receiver can resolve the user
- Fixed canonical_pair_id backfill: SQLite NULL || ':' || x = NULL,
so use COALESCE(home_user_id, id) instead of concatenation
Existing 1-on-1 DM channels created before federation had canonical_pair_id = NULL,
causing the relay to create duplicate channels on receipt of a message. On server
startup, migrations now: (1) compute and set canonical_pair_id for all existing
1-on-1 channels using home_user_id (falling back to regular id for local users),
and (2) merge any duplicate channels that share the same canonical_pair_id by
moving all messages, read states, and members into the oldest channel and deleting
the stale duplicate. Both steps are non-fatal and idempotent.
Federation relay was incorrectly defaulting to disabled, requiring
admins to discover a buried settings toggle. Since federation itself
is opt-in (you connect instances manually), relay should be on by
default. Also migrates existing instances from 0 → 1.
Task 13: Hook server-to-server peering into connectToRemote (non-fatal)
and add federation API namespace to the client (initiate, peers, revoke).
Task 14: Show a transparency notice in the DM welcome header when the
other user is on a remote instance, informing that messages are stored
on both home instances and are not end-to-end encrypted.
Task 15: Add Federation section to the instance settings General panel
with DM relay toggle, TTL config, and a live peer list with revoke
buttons. Also extends InstanceAdminSettings type and the server settings
route to support federationRelayEnabled / federationRelayTtlDays.
Add POST /api/federation/sync for peer instances to catch up on missed
DM mutations after downtime. Uses HMAC-SHA256 authentication (same as
relay), determines shared channels between peers via raw SQL join on
dm_members/users, queries the federation_mutation_log with timestamp
pagination, and returns FederationRelayEvent payloads for each mutation
type (create, update, delete, reaction_add, reaction_remove).
Add federation routes module with four endpoints:
- POST /api/federation/peer/initiate — admin-only, starts peering handshake
with a remote instance (validates origin, generates HMAC secret, sends
server-to-server accept request with 10s timeout)
- POST /api/federation/peer/accept — server-to-server first contact,
rate-limited to 10 req/min per IP, creates/activates peer record
- GET /api/federation/peers — admin-only, lists all peers (hmacSecret excluded)
- DELETE /api/federation/peers/:id — admin-only, revokes peer and cleans outbox
Adds column migrations (dm_channels.canonical_pair_id, dm_messages.source_*,
attachments.source_url, instance_settings.federation_relay_*), CREATE TABLE
for federation_peers/outbox/file_queue/mutation_log, partial/filtered indexes,
and an idempotent backfill that seeds the mutation log from existing DM messages.
Also makes migrateDmMessagesReplyToFk and migrateAttachmentsDmMessageFk
column-aware so table recreation preserves any columns added by earlier
migrations (prevents dropping federation columns on upgrade).
Lower minimum query length from 2 to 1 character so single-letter
searches return results.
Fix dedup to prefer native profiles (homeUserId=null) over replicated
ones. Previously the first-seen result won, which was usually the
local replicated profile (no instance badge, namespaced username).
Now when a native profile is found on the remote instance, it replaces
the replicated copy — showing the clean username with the instance badge.
Previously, voice cleanup only happened in finalizeDisconnect after a
5-second grace period, and only when ALL connections closed. If a user
was logged in on multiple devices and closed the one in voice, the
voice state was never cleaned up — creating a permanent ghost in the
channel sidebar.
Now removeConnection checks if the closing socket is the voice-active
one (tracked via voiceWs map) and immediately cleans up voice state,
broadcasts the leave event, and notifies remaining tabs.
Adds device-switch guardrail to handleVoiceJoin: if the user already
has a voice session on a different WebSocket, that old socket receives
a voice_disconnected/displaced event before the new session takes over.
Also binds voiceWs on the caller socket in handleDmCallStart and on
the acceptor socket in handleDmCallAccept.
The 500 MB cap was arbitrary. Self-hosted admins should decide based
on their own disk space. Global multipart ceiling raised to 5 GB,
validation cap to 5120 MB.
Reads max_upload_size_bytes from instance_settings per-request and
passes it to request.file() so Fastify kills the stream at the
admin's configured limit. Checks file.truncated to properly reject
files that exceed the limit instead of saving corrupted data.
Adds cleanupOldMedia() to the storage janitor and a new endpoint
POST /api/admin/storage/cleanup-media { maxAgeDays, dryRun }.
Deletes chat attachments older than the specified threshold while
preserving profile images.
Rebuilds the attachments table with ON DELETE CASCADE on dm_message_id.
Dangling records are excluded during copy — their files become standard
disk orphans detectable by the storage janitor. Also updates the raw SQL
in index.ts to include width/height/duration columns for fresh installs.
Adds getDanglingAttachments() to find attachment records whose
messageId/dmMessageId points to a deleted message. Updates stats
to show dangling count and cleanup to delete them.
When a federated user's local password hash is stale (e.g. they changed
their password on the home instance and sync failed), the login handler
now falls back to verifying credentials against the home instance. If the
home instance accepts the password, the local hash is silently updated
without touching passwordChangedAt, so existing valid JWTs remain valid.
Add homeInstance, role, joinedAfter, joinedBefore, and sort query
params to GET /api/admin/users. Add GET /api/admin/users/instances
endpoint returning distinct home instance domains.
- Import BITRATE_MATRIX_KBPS from shared constants
- Parse and return bitrateMatrixOverrides in rowToLimits
- Widen updateData type to accept null values
- Raise maxBitrateKbps and minBitrateKbps caps from 50 000 to 1 000 000 kbps
- Add full validation for bitrateMatrixOverrides (null to clear, key/value checks)
Replace hardcoded VALID_RESOLUTIONS/VALID_FRAMERATES with shared STANDARD_RESOLUTIONS/STANDARD_FRAMERATES constants. Update rowToLimits to handle the 'native' resolution value in DB parsing, and update all PATCH validation blocks to accept the expanded resolution/framerate sets and the 'native' option.