When a moderator server-deafens a user, the implied mute is server-imposed
and should display amber (not red) everywhere. Updated VoiceControlBar,
ChannelSidebar, VoiceChannel, and VoiceUser to check isServerDeafened
alongside isServerMuted for amber color and cursor-not-allowed state.
Also includes smart mute/deafen toggle logic (Discord-style coupling),
server-side enforcement of mute/deafen bypass, and cleanup of server
voice state on user departure.
Add ban/unban functionality with BansPanel in space settings, voice
moderation context menu (mute/deafen/disconnect), and fix federated
space settings panels to use origin-aware API client. Show domain
indicators for federated members in MembersPanel.
Replace truncated text labels with globe pill dividers and add
federation badges to remote space icons. Remote spaces now show
rich Tooltips with "SpaceName · host" instead of native title attrs.
Disconnected instances display amber warning indicators throughout.
- Redesign explore cards with banner images, overlapping icons, and frosted fade
- Extract dominant colors from space icons for dynamic banner gradients
- Add space description/banner fields to schema with migration
- Move origin label from banner overlay to content metadata row
- Support space descriptions in settings overview panel
fetchSpaces() now awaits _autoConnectDone before reading the instance
list, preventing a race where it would fetch with an empty/incomplete
set of connected instances during page reload.
Move instance-level administration out of Space Settings into its own
modal. Adds admin UI for instance name, registration toggle, and
discovery toggle. Streaming limits panel relocated from SpaceSettings.
- Add InstanceAdminSettings type and GET/PATCH /api/settings/instance
- Add registration_open DB column (nullable, env var fallback)
- Auth registration and instance info now check DB override
- New InstanceSettings modal with General and Streaming tabs
- Admin shield button in UserAreaPanel (visible to admins only)
- Remove Streaming tab from SpaceSettings
Fix three federation bugs in Space Settings: icon URL was double-wrapped
through home API, upload targeted home instance instead of remote, and
updateSpace used home API client. Also restyle settings panels (User,
Space, Members, Roles, Connected Instances) with consistent section
headers, grouped cards, and improved layout.
Replace inline Save/Discard/Reset buttons with sticky glass-bubble pills
that float at the bottom of scrollable modal content. Make SpaceSettings
tab sidebar sticky with glass material. Convert RoleEditView "Back to
roles" into a sticky glass pill at the top. UserSettings Log Out + Save
always visible in a glass pill with separator. Also includes floating
position hook and popover/tooltip improvements from prior work.
Restructure SpaceSettings modal from 778-line monolith into thin
orchestrator with extracted panel components. Add full role CRUD with
permission editor (create, edit name/color/permissions, delete).
Backend: expose role permissions in GET response, accept permissions
in POST/PATCH role endpoints with BigInt validation, broadcast
pushReadyPayload to all space members on role mutations.
Wire react-easy-crop into CreateSpace so users can crop and zoom before
uploading a space icon. Adds reusable ImageCropModal component and
canvas crop utility for future use in space settings and avatar editing.
- Accept visibility and description fields when creating spaces
- Register creator in connectionManager on space creation for immediate WS broadcasts
- Return per-channel myPermissions and space-level myPermissions from GET /spaces/:id
- Populate permission maps in spaceStore from REST response
- Add reauthenticateInstance flow for tokenless federation placeholders
- Handle expired/missing tokens gracefully in autoConnectAll with visible error state
- Guard syncInstanceList against premature runs before autoConnectAll completes
Remove disabled={isUploading} from the textarea so focus() isn't silently
ignored on a disabled element. Fix ResizeObserver effect deps so the
observer is actually created after the loading spinner transitions to
message content.
Use origin-aware user ID resolution for optimistic voice add/remove
instead of always using the home instance ID. Adds getMyUserIdForOrigin
resolver (same pattern as getApiForOrigin) so joinVoiceChannel and
leaveVoice use the correct federated user ID. Also fixes WS reconnect
voice re-registration, invite URL for remote servers, and chatStore
early-load guard for unmapped channels.
The redirect effect used the shared `channels` array which could contain
stale data from a previously viewed server during async loadServerDetail.
Added channelToServerMap ownership check to ensure channels belong to
the target server before redirecting. Also persists last-visited channel
per server in localStorage for better return navigation.
The Explore page shared /channels/@me with Friends, differentiated by a
showExplore UI flag. When PiP navigated to a voice channel, the stale
flag caused MainContent to render text chat instead of the voice grid.
Replace the flag with a dedicated /explore route so the URL is the
single source of truth.
Joined discoverable servers now appear in a collapsible "Joined" section
at the bottom of the explore page, so the page never feels dead when all
servers have been joined. A mint success banner replaces the old empty
state text.
$client doesn't exist in drizzle-orm 0.33, causing a TypeError and
500 on every /api/servers/explore request. Export the raw better-sqlite3
instance directly from db/index.ts instead.
The echo gate architecture bypassed the serialized mic management chain
and introduced race conditions. Chrome's AEC handles echo properly for
both headphone and speaker users without custom intervention.
Send voice_join to WS server only after LiveKit successfully connects,
not immediately on joinVoiceChannel() or WS reconnect. Background tabs
can reconnect WebSocket but not WebRTC, causing phantom voice users.
Previously, the voice channel sidebar only showed users after a server
round-trip (voice_state_update broadcast). After a deploy/reconnect,
this left the user invisible in the sidebar despite being connected.
Now joinVoiceChannel, leaveVoice, and the WS ready handler all
optimistically update voiceUsers for the local user immediately.
Clear stale currentChannelId when clicking Explore/DMs in ServerSidebar,
and guard AppLayout route effect from clobbering showExplore. Move member
exclusion into SQL for correct pagination/totals, surface allSettled errors,
and show context-aware empty state messages.
Send explicit voice_leave to the old instance when joining voice on a
different origin, preventing stale voice state from showing the user in
two channels. Also make WS reconnect voice re-registration origin-aware
so remote reconnects properly restore voice state.
Replace the unreliable client-side DM dedup loop in populateFromReady with
a creation-time guard (findExistingDmForUser) that checks all instances
before opening a new DM. Guards added to FriendsPage, NewDmModal, and
UserProfilePopout.
Also fixes: corrupted read_states from temp_ optimistic message IDs (server
migration + client-side validation), federation-aware closeDm/addDmMember
API routing, isSelf-based DM member filtering in sidebar/header, and WS
event error isolation.
- Fix DM welcome header avatar using home identity for correct gradient color
- Register DM channel IDs in channelOriginMap so federated DM operations
(reactions, messages, typing) route to the correct instance
- Pass origin when creating DM channels from friends list and WS events
- Add server discovery/explore page with public server listings
- Add server visibility and description fields
Rewrites socialStore to aggregate friends and requests from all connected
instances using Promise.allSettled. Parses user@domain in friend requests
to route to the correct instance. Removes !isHome guards on social WS
events so remote friend requests arrive in real-time. Shows "via hostname"
labels on remote friends/requests in the UI.
Store the original home snowflake ID (homeUserId) during federation replication
so that avatar gradient colors resolve identically across instances. Previously,
replicated users got new snowflake IDs on each instance, causing different
gradient colors. Now Avatar, UserProfilePopout, VoiceUser, StreamTile, and
VoiceChannel all resolve through homeUserId when available. Includes backfill
logic for existing federated users missing the field.
Federated users get a different snowflake ID on remote instances,
causing avatar gradients to mismatch their home identity. Apply
resolveDisplayIdentity/isSelf resolution in VoiceUser, StreamTile,
VoiceChannel sidebar, and MemberSidebar so the current user's avatar
color is consistent across all views.
Move TypingIndicator from document flow (between MessageList and
MessageInput) into MessageInput's outer wrapper using absolute
bottom-full positioning. Fixes indicator being hidden behind the
floating glass input bubble on desktop.
- Include user object in reaction_added WS broadcasts for isSelf() resolution
- Use isSelf() instead of userId comparison for reaction ownership checks
- Load remote server detail after ready event to prevent empty channel list
Add identity.ts with isSelf() and resolveDisplayIdentity() — pure
stateless functions that detect replicated-self using the immutable
(username, homeInstance) composite key. No store lookups, no data
mutation. Fixes wrong avatar gradient and missing edit/delete on
own messages in remote channels.
Also includes: optimistic message dedup fix for cross-instance
messages (content-only matching), federation toast notifications,
Username component with @domain display, invite parser, and
deploy script simplification.
Batch 1 — Bug fixes:
- Fix stale voice state on remote reconnect (clearVoiceUsersForOrigin)
- Fix logout not cleaning remote servers from store
- Fix reply-to asset normalization for remote messages
- Fix HTTPS hardcoded in autoConnectAll (store full origin, legacy fallback)
Batch 2 — Password enforcement + replication flow:
- Add connectToRemote() with home password verification before remote auth
- Auto-cascade: verify home password → register on remote → login fallback
- Replace Register/Login tabs with single password field in ConnectedInstances
- Add DifferentPasswordError for typed fallback-login UI transition
- Fallback login form shown only when remote has different password
The env_val() function uses grep which returns exit code 1 when no
match is found. Under set -euo pipefail, this kills the script.
Adding || true prevents this on upgrade installs with older .env files.
The node -e command to set instance name needs to run from
/app/packages/server where pnpm's symlinked node_modules resolve
better-sqlite3, not from /app root.
Redesign deployment as a single docker-compose with Backspace, Caddy
(auto-HTTPS), and LiveKit (voice/video) using hybrid networking:
Backspace+Caddy on isolated bridge, LiveKit on host mode for WebRTC.
- Add install.sh: interactive installer that handles Docker setup,
domain/DNS verification, secret generation, LiveKit config, and
deployment with health-check wait
- Add Caddyfile: static reverse proxy config using Caddy env vars,
routes /livekit/* to host-mode LiveKit via host.docker.internal
- Rewrite docker-compose.yml: all-in-one with profiles (voice),
no external volumes/networks, bind mount ./data for visibility
- Fix livekit.ts: use LIVEKIT_URL env var directly instead of
Host-header derivation that made the env var dead code
- Fix Dockerfile: health check reads $PORT dynamically
- Update .env.example: add DOMAIN, COMPOSE_PROFILES documentation
- Update .gitignore: add livekit.yaml (contains secrets)
Phase 5 of multi-instance federation. Adds a two-layer fix:
Layer 1 — Data ingestion normalization: Remote instance user avatars,
server icons, and attachment filenames are rewritten to absolute URLs
when entering the app (via WebSocket events or API responses), so all
downstream components render them correctly without changes.
Layer 2 — Outbound action routing: wsSend calls (voice join/leave/status,
typing) and file uploads in UI components now route through the correct
instance based on the active channel's origin.
Two fixes addressing architectural review feedback:
1. Snowflake ID collisions: Replace process.pid-based worker ID with a
cryptographically random value (0-1023) generated once at first boot
and persisted to instance_settings.worker_id. Eliminates deterministic
ID collisions between Docker instances that all run as PID 1.
2. Reaction API leak: Revert addReaction/removeReaction signatures to
(messageId, emoji) — the store now resolves the channel internally by
scanning its message cache, keeping routing logic out of the UI layer.
Refactor the WebSocket layer from a singleton connection to a connection
map supporting N concurrent instances. Each connection has its own
heartbeat worker, reconnect state, and token. Stores are now
instance-aware: serverStore merges servers by origin, chatStore routes
API calls through the correct client, and instanceStore triggers WS
connect/disconnect on add/remove. DM, social, and voice events remain
home-only.
Introduce instanceStore (Zustand) with full federation lifecycle:
probe remote instances, register with username collision fallback,
login to existing accounts, sync instance list across all connected
instances, auto-reconnect from cached tokens on login/page load, and
cleanup on logout. Add ConnectedInstances component to User Settings
with home instance card, remote instance management, and inline
add-instance flow (URL probe → register/login → connected).