ff39ab077dcabab07194ff99e379199ca50c5ea2
Client-side DM origin failover on WS disconnect (backlog #10). When a remote instance's WebSocket drops mid-session, every DM pinned to that origin is re-keyed to a connected sibling that mirrors the same federated DM via S2S replication. Covers the channel-ID-per-origin reality (each instance assigns its own local Snowflake; only federatedId is shared) by keeping a `dmAlternatives: Map<federatedId, Map<origin, localChannelId>>` on spaceStore, populated by every `ready` payload regardless of dedup outcome. On transition, `rekeyDmChannel` atomically renames the DM across spaceStore (dmChannels / channelOriginMap / channelLastMessageIds / dmAlternatives), chatStore (messages / hasMore / scrollPositions / channelAccessTimes / typingUsers / readStates / unreadChannels / currentChannelId), and the URL (history.replaceState when viewing the rekeyed DM). Triggers: setInstanceStatus on connected→disconnected|error, and disconnectInstance / forceRemoveEntry before removeInstanceSpaces. Voice state (activeDmCall / outgoingCall / incomingCall) is intentionally not rewritten — LiveKit rooms can't migrate across origins. As an in-scope adjacent fix (§3.11 of the spec), `dm_message_created` now consults `dmAlternatives` before its legacy 2-member-identity fallback via a new `resolveDmChannelId(rawId)` helper. This closes a pre-existing phantom-sidebar-entry bug for group DMs in multi-instance sessions and handles post-failover routing when the reconnected original origin's WS still addresses the DM by its old local id. Live verification on Pi+VM (youruser@nova.ddns.net with Orbit.Backspace as remote): (1) baseline — DM pinned to home, WS drop of remote is a no-op, reconnect clean, no flap; (2) forced-rekey path — WebSocket construction delayed on wss://nova.ddns.net via a client-side patch so orbit's `ready` arrived first, pinning the Nova DM to orbit with orbit's local id. Stopping the orbit container triggered failoverDmOriginsFromDisconnected; URL auto-swapped from `/channels/@me/<orbit-local-id>` to `/channels/@me/<nova-local-id>` via history.replaceState, the chat view re-fetched from nova via the new primary id, and subsequent message sends routed to nova. Restart of orbit left the pin on nova — no re-home flap (§3.6). Group-DM phantom fix covered by unit tests (9 in dmOriginFailover.test.ts plus contract test); not exercised live because it requires concurrent delivery from the non-primary origin's WS, which the forced-rekey session didn't naturally produce. Design: internal notes Plan: internal notes Pre-existing test failures on main (keybindStore, FriendsPage, InviteModal, JoinSpace — 12 tests) are unchanged by this branch.
Backspace
Open-source, self-hosted Discord alternative built with TypeScript.
Features
- Real-time text messaging with WebSocket
- Servers, channels, and role-based permission management
- Voice and video chat via LiveKit
- Screen sharing with configurable quality (VP9)
- Direct messages (1-on-1 and group DMs up to 10)
- DM voice/video calls with ringing
- Friend system with requests
- File uploads and image sharing
- Markdown message formatting with syntax highlighting
- Message reactions, replies, and editing
- Typing indicators, presence status, and read states
- Invite system with shareable codes
- Instance-level admin panel (streaming limits)
- Desktop app (Electron)
- Mobile-responsive web UI
- Docker deployment
Tech Stack
| Layer | Technology |
|---|---|
| Backend | Fastify + TypeScript |
| Database | SQLite (better-sqlite3) + Drizzle ORM |
| Auth | JWT + bcrypt |
| Real-time | WebSocket (ws) |
| Frontend | React 18 + Tailwind CSS + Zustand |
| Voice/Video | LiveKit |
| Desktop | Electron |
| Build | Vite + pnpm workspaces |
Quick Start with Docker
# Clone the repository
git clone https://github.com/your-username/backspace.git
cd backspace
# Create environment file
cp .env.example .env
# Generate a JWT secret
echo "JWT_SECRET=$(openssl rand -hex 32)" >> .env
# Start Backspace
docker compose up -d
Open http://localhost:3000 in your browser. A default server "Backspace" is created automatically.
Default admin account: admin / admin123 (change this after first login).
Development Setup
Prerequisites
- Node.js 20+
- pnpm 8+
Install
pnpm install
Configure
cp .env.example .env
# Edit .env with your settings (generate a JWT_SECRET)
Run
# Start both server and web dev server
pnpm dev
# Or start individually
pnpm dev:server # API server on :3005
pnpm dev:web # Vite dev server on :5173
Build
pnpm build
This builds the shared types, server, and web frontend. The server serves the built frontend in production mode.
Project Structure
Backspace/
├── packages/
│ ├── shared/ # Shared TypeScript types & permissions
│ ├── server/ # Fastify API + WebSocket server
│ ├── web/ # React frontend (Vite + Tailwind)
│ └── desktop/ # Electron desktop app
├── data/ # SQLite DB + uploads (created at runtime)
├── Dockerfile
├── docker-compose.yml
└── .env.example
Voice & Video
Voice and video requires a LiveKit server. Set these in your .env:
LIVEKIT_URL=wss://your-livekit-server
LIVEKIT_API_KEY=your-api-key
LIVEKIT_API_SECRET=your-api-secret
Without LiveKit configured, text chat works fully but voice/video channels will not connect.
API
The server exposes a REST API and WebSocket endpoint:
- REST API:
http://localhost:3000/api/* - WebSocket:
ws://localhost:3000/ws - Health check:
GET /api/health
See CLAUDE.md for the full API reference.
Desktop App
The Electron desktop app wraps the web UI and adds system tray, notifications, and native window controls.
cd packages/desktop
pnpm build:ts # Compile TypeScript
pnpm dev # Run in development
pnpm build # Package for distribution
License
MIT
Languages
TypeScript
96.8%
HTML
1.3%
Shell
1.1%
JavaScript
0.4%
CSS
0.3%