<!-- Thanks for contributing. Keep one logical change per pull request. -->
## What this changes
<!-- A short summary of the change and the problem it solves. Link the issue it addresses. -->
Closes #
## Type of change
- [ ] Bug fix
- [ ] New feature
- [ ] Refactor or cleanup
- [ ] Documentation
- [ ] Other
## Checklist
- [ ]`pnpm build` succeeds (shared types, server, and web all build)
- [ ]`pnpm dev` starts the server and web client without errors
- [ ] Tests pass where applicable (`pnpm test`)
- [ ] I updated the relevant `docs/systems/` spec if this changed schema, API routes, WebSocket events, the federation protocol, permissions, or the design system
- [ ] This change resolves the correct federated identity where it compares IDs, checks permissions, or talks to remote servers (no assumption of a single global user ID)
- [ ] I have read and agree to the [CLA](../CLA.md)
## Notes for reviewers
<!-- Anything that helps review: screenshots for UI, migration notes, edge cases, follow-ups. -->
<sub><b>Federation admin</b> — manage peered instances, relay, and secret rotation.</sub>
<sub><b>Federation admin.</b> Manage peered instances, relay, and secret rotation.</sub>
</td>
</td>
</tr>
</tr>
</table>
</table>
@@ -106,22 +106,22 @@ You own the server, the data, and the network it federates into.
- Message reactions, replies, editing, deletion, and per-message mark-as-unread
- Message reactions, replies, editing, deletion, and per-message mark-as-unread
- Rich link embeds (YouTube, Vimeo, Spotify, and generic OpenGraph) with SSRF-protected scraping, plus GIF search (Klipy)
- Rich link embeds (YouTube, Vimeo, Spotify, and generic OpenGraph) with SSRF-protected scraping, plus GIF search (Klipy)
- Typing indicators, unread badges, and presence
- Typing indicators, unread badges, and presence
- Direct messages — 1-on-1 and group DMs (up to 10 people), with voice/video calls (ring / accept / reject)
- Direct messages: 1-on-1 and group DMs (up to 10 people), with voice/video calls (ring, accept, reject)
**Voice, video & screen sharing** (via [LiveKit](https://livekit.io/)):
**Voice, video, and screen sharing** (via [LiveKit](https://livekit.io/)):
- Screen sharing up to 4K / 120fps — VP9 by default, an optional hardware-accelerated H.264 mode, and a VP8 simulcast fallback
- Screen sharing up to 4K/120fps: VP9 by default, an optional hardware-accelerated H.264 mode, and a VP8 simulcast fallback
- Per-stream quality controls — resolution, frame rate, codec, and bitrate, within admin-set bounds
- Per-stream quality controls: resolution, frame rate, codec, and bitrate, within admin-set bounds
- Independent 0–200% volume for every participant *and* every screen-share
- Independent 0-200% volume for every participant and every screen-share
- RNNoise noise suppression (on by default), plus echo-cancellation and auto-gain toggles and mic/speaker device selection
- RNNoise noise suppression (on by default), plus echo-cancellation and auto-gain toggles and mic/speaker device selection
- Live connection inspector — per-participant bitrate, codec, ping, packet loss, and jitter — plus a per-tile badge showing each stream's measured resolution/frame-rate
- Live connection inspector for per-participant bitrate, codec, ping, packet loss, and jitter, plus a per-tile badge showing each stream's measured resolution and frame-rate
- Screen-share viewer detection ("who's watching") and auto-ducking that lowers stream audio when someone speaks
- Screen-share viewer detection ("who's watching") and auto-ducking that lowers stream audio when someone speaks
- Selective subscription — mute or stop watching any camera/stream to save bandwidth
- Selective subscription: mute or stop watching any camera or stream to save bandwidth
- Push-to-talk and fully customizable keybinds (including mouse buttons), in the browser and the desktop app
- Push-to-talk and fully customizable keybinds (including mouse buttons), in the browser and the desktop app
- Picture-in-Picture for voice and video
- Picture-in-Picture for voice and video
### Organization
### Organization
- Spaces with channel categories
- Spaces with channel categories
- Role-based permissions — bitwise RBAC with category- and channel-level overrides
- Role-based permissions: bitwise RBAC with category- and channel-level overrides
- Customizable user sidebar layout, with personal color-coded folders that group whole spaces
- Customizable user sidebar layout, with personal color-coded folders that group whole spaces
- Space discovery (public, request-to-join, and private)
- Space discovery (public, request-to-join, and private)
- Shareable invite codes
- Shareable invite codes
@@ -132,8 +132,8 @@ You own the server, the data, and the network it federates into.
- Mutual friends and mutual spaces
- Mutual friends and mutual spaces
- User profiles with banner, bio, and accent color
- User profiles with banner, bio, and accent color
- Presence and rich activities (playing, listening, watching, streaming, custom)
- Presence and rich activities (playing, listening, watching, streaming, custom)
- Manual status — Online, Idle, or Do Not Disturb — with a custom status message
- Manual status (Online, Idle, or Do Not Disturb) with a custom status message
- Privacy controls — toggle discoverability and activity-status sharing
- Privacy controls: toggle discoverability and activity-status sharing
### Moderation
### Moderation
- Bans with reason and moderator attribution (who, why, and when)
- Bans with reason and moderator attribution (who, why, and when)
@@ -144,7 +144,7 @@ You own the server, the data, and the network it federates into.
### Federation
### Federation
- Multi-instance peering with HMAC-signed server-to-server requests
- Multi-instance peering with HMAC-signed server-to-server requests
- Cross-instance DMs — messages, reactions, and membership relay
- Cross-instance DMs: messages, reactions, and membership relay
- Cross-instance friends and presence
- Cross-instance friends and presence
- File replication with size validation
- File replication with size validation
- Background workers for outbox delivery, file download, peer health, and cleanup
- Background workers for outbox delivery, file download, peer health, and cleanup
@@ -152,27 +152,27 @@ You own the server, the data, and the network it federates into.
### Platform
### Platform
- File uploads with image thumbnails (via `sharp`), drag-and-drop and paste-to-upload, and in-app avatar/banner cropping
- File uploads with image thumbnails (via `sharp`), drag-and-drop and paste-to-upload, and in-app avatar/banner cropping
- Message search with `from:`, `has:`, `before:`, and `after:` filters, plus jump-to-message
- Message search with `from:`, `has:`, `before:`, and `after:` filters, plus jump-to-message
- Admin panel — instance settings, user management, registration controls, storage management, and federation/peering, plus granular streaming controls (a per-resolution × per-frame-rate bitrate matrix, min/max caps, quality-slider step, and an optional user-set-bitrate mode)
- Admin panel: instance settings, user management, registration controls, storage management, and federation/peering, plus granular streaming controls (a per-resolution by per-frame-rate bitrate matrix, min/max caps, quality-slider step, and an optional user-set-bitrate mode)
- Automatic SQLite backups (pre-migration, scheduled, and manual) with restore tooling
- Automatic SQLite backups (pre-migration, scheduled, and manual) with restore tooling
- Electron desktop app (Windows, macOS, Linux) with global keybinds (push-to-talk, mute, deafen) and activity detection
- Electron desktop app (Windows, macOS, Linux) with global keybinds (push-to-talk, mute, deafen) and activity detection
- Native desktop notifications and unread badge counts
- Native desktop notifications and unread badge counts
- Mobile-responsive web UI with a dedicated touch layout (bottom navigation, swipe gestures, full-screen views)
- Mobile-responsive web UI with a dedicated touch layout (bottom navigation, swipe gestures, full-screen views)
- Installable PWA — add it to your phone's home screen to run it as a standalone app, with service-worker caching and an offline message queue (messages send once you reconnect)
- Installable PWA: add it to your phone's home screen to run it as a standalone app, with service-worker caching and an offline message queue (messages send once you reconnect)
- Account management — password change and account deletion with safeguards
- Account management: password change and account deletion with safeguards
## Installation
## Installation
The intended way to deploy Backspace is the **interactive installer** — it
The intended way to deploy Backspace is the **interactive installer**. It
configures everything (`.env`, secrets, HTTPS, optional voice) and brings the
configures everything (`.env`, secrets, HTTPS, optional voice) and brings the
stack up for you. It **auto-detects your environment** and picks one of three
stack up for you. It **auto-detects your environment** and picks one of three
deployment modes — the default "All-in-One" (below) needs nothing but a host and
deployment modes. The default "All-in-One" (below) needs nothing but a host and
a domain, but if ports 80/443 are already taken (an existing reverse proxy, a
a domain, but if ports 80/443 are already taken (an existing reverse proxy, a
tunnel, another app) the installer steers you to the right mode instead of
tunnel, another app) the installer steers you to the right mode instead of
dead-ending. See [Deployment modes](#deployment-modes) for the full picture.
dead-ending. See [Deployment modes](#deployment-modes) for the full picture.
By default the installer **pulls a prebuilt multi-architecture image** from the
By default the installer **pulls a prebuilt multi-architecture image** from the
GitHub Container Registry (`linux/amd64` + `linux/arm64`), so weak or ARM boxes
GitHub Container Registry (`linux/amd64` + `linux/arm64`), so weak or ARM boxes
(a Raspberry Pi) skip the heavy local build — it falls back to building from
(a Raspberry Pi) skip the heavy local build. It falls back to building from
source automatically if the image can't be pulled.
source automatically if the image can't be pulled.
### Requirements
### Requirements
@@ -181,7 +181,7 @@ source automatically if the image can't be pulled.
- A **domain name** for your instance. In the default All-in-One mode it must
- A **domain name** for your instance. In the default All-in-One mode it must
point at the host's public IP (Caddy obtains HTTPS certificates for it
point at the host's public IP (Caddy obtains HTTPS certificates for it
automatically); behind your own reverse proxy or a tunnel it points at that
automatically); behind your own reverse proxy or a tunnel it points at that
edge instead — see [Deployment modes](#deployment-modes).
edge instead. See [Deployment modes](#deployment-modes).
- The ability to open the firewall ports in step 2 (All-in-One), or a reverse
- The ability to open the firewall ports in step 2 (All-in-One), or a reverse
proxy / tunnel already terminating HTTPS for you.
proxy / tunnel already terminating HTTPS for you.
@@ -203,34 +203,34 @@ The installer walks you through everything interactively:
### 2. Open the firewall ports
### 2. Open the firewall ports
Open these on the host — and, if it's behind a router, port-forward them to the host:
Open these on the host (and, if it's behind a router, port-forward them to the host):
| **`allinone`** (default) | Ports 80/443 are free and you have a domain | The bundled **Caddy** (automatic Let's Encrypt) | ✅ with UDP media ports open |
| **`allinone`** (default) | Ports 80/443 are free and you have a domain | The bundled **Caddy** (automatic Let's Encrypt) | Yes, with UDP media ports open |
| **`proxy`** | You already run a reverse proxy (nginx, Traefik, Caddy, Nginx Proxy Manager, SWAG…) | **Your** reverse proxy | ✅ if you also proxy `/livekit` and open the media ports |
| **`proxy`** | You already run a reverse proxy (nginx, Traefik, Caddy, Nginx Proxy Manager, SWAG…) | **Your** reverse proxy | Yes, if you also proxy `/livekit` and open the media ports |
| **`tunnel`** | You expose the box through a tunnel (Cloudflare Tunnel, Tailscale…) | The **tunnel** provider | ❌ WebRTC/UDP can't traverse a tunnel |
| **`tunnel`** | You expose the box through a tunnel (Cloudflare Tunnel, Tailscale…) | The **tunnel** provider | No, WebRTC/UDP can't traverse a tunnel |
In `proxy` and `tunnel` mode the bundled Caddy is **not** started; instead the app
In `proxy` and `tunnel` mode the bundled Caddy is **not** started; instead the app
is published on **`127.0.0.1:APP_PORT`** (loopback only — never exposed directly)
is published on **`127.0.0.1:APP_PORT`** (loopback only, never exposed directly)
for your proxy or tunnel to forward to. This is driven by a small overlay,
for your proxy or tunnel to forward to. This is driven by a small overlay,
`docker-compose.proxy.yml`, which the installer wires in for you by setting
`docker-compose.proxy.yml`, which the installer wires in for you by setting
`COMPOSE_FILE=docker-compose.yml:docker-compose.proxy.yml` in `.env` — so every
`COMPOSE_FILE=docker-compose.yml:docker-compose.proxy.yml` in `.env`, so every
later `docker compose …` command in the directory keeps working with no `-f`
later `docker compose …` command in the directory keeps working with no `-f`
flags. The installer auto-picks a free `APP_PORT` (3000/8080 are often taken);
flags. The installer auto-picks a free `APP_PORT` (3000/8080 are often taken);
override it with `APP_PORT=…`.
override it with `APP_PORT=…`.
@@ -292,7 +292,7 @@ override it with `APP_PORT=…`.
The installer prints ready-to-paste config for your mode at the end. The
The installer prints ready-to-paste config for your mode at the end. The
canonical snippets are below.
canonical snippets are below.
### Mode 2 — behind your own reverse proxy
### Mode 2: behind your own reverse proxy
The app answers plain HTTP on `127.0.0.1:APP_PORT`; your proxy terminates TLS and
The app answers plain HTTP on `127.0.0.1:APP_PORT`; your proxy terminates TLS and
forwards to it. Every snippet already includes the three things people get wrong:
forwards to it. Every snippet already includes the three things people get wrong:
@@ -302,7 +302,7 @@ scheme/IP), and a **body-size limit** matching `MAX_UPLOAD_SIZE` (default 100 MB
Replace `chat.example.com` and `8080` with your domain and `APP_PORT`.
Replace `chat.example.com` and `8080` with your domain and `APP_PORT`.
**nginx**— the `map` goes in `http { }` once; the `server` block per site:
**nginx.**The `map` goes in `http { }` once; the `server` block per site:
# Voice only — add a higher-priority router + stripPrefix middleware for
# Voice only: add a higher-priority router + stripPrefix middleware for
# PathPrefix(`/livekit`) → http://127.0.0.1:7880.
# PathPrefix(`/livekit`) → http://127.0.0.1:7880.
```
```
@@ -382,9 +382,9 @@ by hand. In **Nginx Proxy Manager**, add a **Proxy Host**:
|-------|-------|
|-------|-------|
| **Domain Names** | `chat.example.com` |
| **Domain Names** | `chat.example.com` |
| **Scheme** | `http` |
| **Scheme** | `http` |
| **Forward Hostname / IP** | `127.0.0.1` — but **if NPM runs in Docker**, `127.0.0.1` is NPM's *own* container. Use the host's LAN IP, or `host.docker.internal` with `extra_hosts: ["host.docker.internal:host-gateway"]` on the NPM container. |
| **Forward Hostname / IP** | `127.0.0.1`, but **if NPM runs in Docker**, `127.0.0.1` is NPM's *own* container. Use the host's LAN IP, or `host.docker.internal` with `extra_hosts: ["host.docker.internal:host-gateway"]` on the NPM container. |
| **Forward Port** | your `APP_PORT` (e.g. `8080`) |
| **Forward Port** | your `APP_PORT` (e.g. `8080`) |
| `DEPLOY_MODE` | no | `allinone` | `allinone` \| `proxy` \| `tunnel` — see [Deployment modes](#deployment-modes) |
| `DEPLOY_MODE` | no | `allinone` | `allinone` \| `proxy` \| `tunnel`, see [Deployment modes](#deployment-modes) |
| `APP_PORT` | no | auto | `proxy`/`tunnel` only: host loopback port the app is published on |
| `APP_PORT` | no | auto | `proxy`/`tunnel` only: host loopback port the app is published on |
| `PORT` | no | `3000` | App listen port (behind Caddy in Docker) |
| `PORT` | no | `3000` | App listen port (behind Caddy in Docker) |
| `HOST` | no | `0.0.0.0` | Bind address |
| `HOST` | no | `0.0.0.0` | Bind address |
| `REGISTRATION_OPEN` | no | `true` | Set `false` to close signups after setup |
| `REGISTRATION_OPEN` | no | `true` | Set `false` to close signups after setup |
| `MAX_UPLOAD_SIZE` | no | `104857600` | Max upload size in bytes (100 MB; 90 MB in `tunnel` mode) |
| `MAX_UPLOAD_SIZE` | no | `104857600` | Max upload size in bytes (100 MB; 90 MB in `tunnel` mode) |
| `BACKSPACE_IMAGE` / `BACKSPACE_IMAGE_TAG` | no | `ghcr.io/thezwiss/backspace` / `latest` | Prebuilt image to pull; pin a tag or point at your fork's registry |
| `BACKSPACE_IMAGE` / `BACKSPACE_IMAGE_TAG` | no | `ghcr.io/thezwiss/backspace` / `latest` | Prebuilt image to pull; pin a tag or point at your fork's registry |
Enabling voice also requires opening the WebRTC ports (`3478/UDP`, `7881/TCP`,
Enabling voice also requires opening the WebRTC ports (`3478/UDP`, `7881/TCP`,
`50000–60000/UDP`) — see [Open the firewall ports](#2-open-the-firewall-ports).
`50000–60000/UDP`). See [Open the firewall ports](#2-open-the-firewall-ports).
Without LiveKit configured, everything else — text, federation, DMs, uploads,
Without LiveKit configured, everything else works fully (text, federation, DMs,
search — works fully; only voice/video channels won't connect.
uploads, search); only voice/video channels won't connect.
## Federation
## Federation
@@ -558,11 +558,11 @@ Grab the installer for your platform from the
| Platform | File | Notes |
| Platform | File | Notes |
|----------|------|-------|
|----------|------|-------|
| Windows | `Backspace-<version>.exe` | Universal installer (x64 + arm64). SmartScreen may warn on first run — choose "More info" → "Run anyway". Auto-updates. |
| Windows | `Backspace-<version>.exe` | Universal installer (x64 + arm64). SmartScreen may warn on first run; choose "More info" → "Run anyway". Auto-updates. |
| macOS | `Backspace-<version>-arm64.dmg` (Apple Silicon) / `Backspace-<version>-x64.dmg` (Intel) | Builds are currently **unsigned**: on first launch, right-click the app → **Open** → **Open**. Auto-update is not available on macOS yet — check the releases page for new versions. |
| macOS | `Backspace-<version>-arm64.dmg` (Apple Silicon) / `Backspace-<version>-x64.dmg` (Intel) | Builds are currently **unsigned**: on first launch, right-click the app → **Open** → **Open**. Auto-update is not available on macOS yet, so check the releases page for new versions. |
| Linux | `Backspace-<version>-x86_64.AppImage` / `-arm64.AppImage`, or `.deb` (`amd64` / `arm64`) | AppImage auto-updates; `.deb` installs update via new releases. |
| Linux | `Backspace-<version>-x86_64.AppImage` / `-arm64.AppImage`, or `.deb` (`amd64` / `arm64`) | AppImage auto-updates; `.deb` installs update via new releases. |
On first launch the app asks for your instance URL — enter the address of the
On first launch the app asks for your instance URL. Enter the address of the
Backspace server you use (e.g. `https://chat.example.com`).
Backspace server you use (e.g. `https://chat.example.com`).
### Building from source
### Building from source
@@ -579,14 +579,14 @@ Cross-platform builds are produced for Windows, macOS, and Linux. See
## Mobile
## Mobile
Backspace works on mobile today — just open your instance in a phone browser.
Backspace works on mobile today. Just open your instance in a phone browser.
The UI has a dedicated touch layout (bottom navigation, swipe gestures, and
The UI has a dedicated touch layout (bottom navigation, swipe gestures, and
full-screen views), and because it ships as an installable **PWA** you can use
full-screen views), and because it ships as an installable **PWA** you can use
your browser's **Add to Home Screen** to install it as a standalone app: its own
your browser's **Add to Home Screen** to install it as a standalone app: its own
icon, no browser chrome, and an offline message queue that flushes when you
icon, no browser chrome, and an offline message queue that flushes when you
reconnect.
reconnect.
Native **iOS and Android app-store apps are planned** — once the project gains
Native **iOS and Android app-store apps are planned**, once the project gains
traction and the funding for the developer-program licenses is secured. Until
traction and the funding for the developer-program licenses is secured. Until
then, the installable PWA is the supported way to run Backspace on a phone.
then, the installable PWA is the supported way to run Backspace on a phone.
@@ -596,10 +596,10 @@ Backspace is a TypeScript monorepo managed with pnpm workspaces.
| Video and screen share | Yes, up to 4K/120fps within admin bounds | Yes | Limited | Partial | Yes | No |
| Per-stream media controls (codec, bitrate, resolution) | Yes | No | No | No | Partial | Some audio |
| Federation between servers | Yes, between Backspace instances | No | No | No | Yes, open standard | No |
| Native mobile apps | Installable PWA | Yes | Yes | Desktop client only | Yes | Yes |
| Runs on a Raspberry Pi | Yes (prebuilt arm64 image) | No | Yes | Yes | Yes | Yes |
"Partial" and "Limited" mean the capability exists but is less complete or less
polished than the leaders in that row at the time of writing. Check the current
state of each project.
## Backspace vs Discord
Discord is the reference experience and has the ecosystem, the bots, and the user
base. It is also proprietary, you cannot host it, and you do not own the data or
the moderation policy. Backspace exists for the people who want the Discord shape
without giving up ownership. You trade the ecosystem and the network effect for
control of the server, the data, and the rules. If you want the biggest community
and the deepest bot ecosystem, use Discord. If you want to own your instance, use
Backspace.
## Backspace vs Revolt
Revolt is the closest peer: an open-source, self-hostable, Discord-style chat with
an active community and a Rust backend. The main differences are the media stack
and federation. Backspace is built around a full voice and video control surface
(per-stream codec, bitrate, and resolution, RNNoise, a live connection inspector,
screen share up to 4K/120fps within admin limits) and supports peering independent
instances. If high-quality voice and screen sharing or cross-instance federation
are central to you, Backspace is aimed squarely at that. If you want a larger
existing community and a longer track record, look at Revolt.
## Backspace vs Spacebar
Spacebar reimplements the Discord backend so the actual Discord client can talk to
a server you host. That is a clever path to instant client familiarity, and it is
the right pick if using the real Discord app against your own backend is the goal.
The trade-off is that it inherits Discord's client and its constraints, and its
voice stack is still maturing. Backspace ships its own client and its own voice
and video stack, and it is federation-first rather than Discord-protocol-first.
## Backspace vs Matrix and Element
Matrix is the mature, standardized answer to open federation, and Element is its
best-known client. If interoperable, standards-based federation across many
different server and client implementations is your priority, Matrix is the
stronger choice and Backspace does not try to replace it. Backspace federation is
newer, simpler, and currently peers Backspace instances with each other rather
than speaking an open cross-ecosystem protocol. Where Backspace differs is the
experience: a tightly integrated Discord-style client with a purpose-built media
control surface, rather than a protocol with many clients of varying polish.
## Backspace vs Mumble
Mumble is outstanding at exactly one thing: low-latency voice for groups, self
hosted, lightweight. It has no rich text platform, no video, and no federation.
If all you need is the best self-hosted push-to-talk voice, Mumble is a great,
proven choice. Backspace is a full communication platform (text, voice, video,
files, social, federation) rather than a dedicated voice server, so pick it when
you want more than voice.
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.