` with:
-
- ```mdx
- import myShot from '../../assets/screenshots/section/my-shot.png';
+Capture the current client at a readable window size and save PNG or WebP files under `public/`. Use original sample assets from the parent e2e repository’s `assets/sample-profiles/` directory.
-
- ```
+The October 2026 desktop captures render the actual React client in Edge with deterministic sample users, avatars, messages, and settings. Native responses are fixtures; these images demonstrate the UI and do not verify a live server operation. The screen-sharing capture uses the client’s preview harness.
-4. Run `npm run build` to confirm the image is included and the
- build passes.
+Add an image with descriptive alt text to the relevant guide, inspect the rendered result, and run `npm run build`. Remove unused superseded screenshots.
## License
diff --git a/astro.config.mjs b/astro.config.mjs
index 7cbe8da..84005a1 100644
--- a/astro.config.mjs
+++ b/astro.config.mjs
@@ -44,7 +44,7 @@ export default defineConfig({
starlight({
title: "Fancy Mumble",
description:
- "Documentation for the Fancy Mumble app, server, and Docker image. Voice chat reimagined for 2026.",
+ "Documentation for the Fancy Mumble app and Starling server. Voice chat reimagined for 2026.",
logo: {
src: "./src/assets/logo.svg",
replacesTitle: false,
@@ -168,6 +168,7 @@ export default defineConfig({
badge: { text: "Ops", variant: "note" },
items: [
{ label: "Docker quick start", link: "/server/docker/" },
+ { label: "Migrating to Starling", link: "/server/migrating-to-starling/" },
{ label: "First-run setup", link: "/server/wizard/" },
{ label: "Configuration reference", link: "/server/config/" },
{ label: "Ports & networking", link: "/server/network/" },
diff --git a/public/android.jpg b/public/android.jpg
deleted file mode 100644
index 8dee3ff..0000000
Binary files a/public/android.jpg and /dev/null differ
diff --git a/public/mainpage.png b/public/mainpage.png
index aa6e0e9..572bc04 100644
Binary files a/public/mainpage.png and b/public/mainpage.png differ
diff --git a/public/preview.png b/public/preview.png
index aa6e0e9..9bf0e7b 100644
Binary files a/public/preview.png and b/public/preview.png differ
diff --git a/public/screenshot-admin-acl-1.png b/public/screenshot-admin-acl-1.png
new file mode 100644
index 0000000..6570145
Binary files /dev/null and b/public/screenshot-admin-acl-1.png differ
diff --git a/public/screenshot-admin-bans-1.png b/public/screenshot-admin-bans-1.png
new file mode 100644
index 0000000..1aca903
Binary files /dev/null and b/public/screenshot-admin-bans-1.png differ
diff --git a/public/screenshot-admin-emotes-1.png b/public/screenshot-admin-emotes-1.png
new file mode 100644
index 0000000..f377732
Binary files /dev/null and b/public/screenshot-admin-emotes-1.png differ
diff --git a/public/screenshot-admin-marketplace-1.png b/public/screenshot-admin-marketplace-1.png
new file mode 100644
index 0000000..6d00018
Binary files /dev/null and b/public/screenshot-admin-marketplace-1.png differ
diff --git a/public/screenshot-admin-onboarding-1.png b/public/screenshot-admin-onboarding-1.png
new file mode 100644
index 0000000..59eb78f
Binary files /dev/null and b/public/screenshot-admin-onboarding-1.png differ
diff --git a/public/screenshot-admin-roles-1.png b/public/screenshot-admin-roles-1.png
new file mode 100644
index 0000000..8009c28
Binary files /dev/null and b/public/screenshot-admin-roles-1.png differ
diff --git a/public/screenshot-admin-users-1.png b/public/screenshot-admin-users-1.png
new file mode 100644
index 0000000..976db4d
Binary files /dev/null and b/public/screenshot-admin-users-1.png differ
diff --git a/public/screenshot-getting-started-connect-1.png b/public/screenshot-getting-started-connect-1.png
index 5016cca..c6bcf83 100644
Binary files a/public/screenshot-getting-started-connect-1.png and b/public/screenshot-getting-started-connect-1.png differ
diff --git a/public/screenshot-getting-started-connect-2.png b/public/screenshot-getting-started-connect-2.png
deleted file mode 100644
index bb88c34..0000000
Binary files a/public/screenshot-getting-started-connect-2.png and /dev/null differ
diff --git a/public/screenshot-getting-started-connect-3.png b/public/screenshot-getting-started-connect-3.png
deleted file mode 100644
index fa5ce83..0000000
Binary files a/public/screenshot-getting-started-connect-3.png and /dev/null differ
diff --git a/public/screenshot-getting-started-connect-4.png b/public/screenshot-getting-started-connect-4.png
deleted file mode 100644
index 6f81357..0000000
Binary files a/public/screenshot-getting-started-connect-4.png and /dev/null differ
diff --git a/public/screenshot-getting-started-first-call-1.png b/public/screenshot-getting-started-first-call-1.png
index ab7340c..572bc04 100644
Binary files a/public/screenshot-getting-started-first-call-1.png and b/public/screenshot-getting-started-first-call-1.png differ
diff --git a/public/screenshot-getting-started-first-call-2.png b/public/screenshot-getting-started-first-call-2.png
index e3ec2fa..340e843 100644
Binary files a/public/screenshot-getting-started-first-call-2.png and b/public/screenshot-getting-started-first-call-2.png differ
diff --git a/public/screenshot-getting-started-install-2.png b/public/screenshot-getting-started-install-2.png
index 915bf16..8abaa0a 100644
Binary files a/public/screenshot-getting-started-install-2.png and b/public/screenshot-getting-started-install-2.png differ
diff --git a/public/screenshot-server-features-link-previews-1.png b/public/screenshot-server-features-link-previews-1.png
index 6c5bdf8..18c96fc 100644
Binary files a/public/screenshot-server-features-link-previews-1.png and b/public/screenshot-server-features-link-previews-1.png differ
diff --git a/public/screenshot-server-features-persistent-chat-1.png b/public/screenshot-server-features-persistent-chat-1.png
index 0760726..ab3949b 100644
Binary files a/public/screenshot-server-features-persistent-chat-1.png and b/public/screenshot-server-features-persistent-chat-1.png differ
diff --git a/public/screenshot-server-features-persistent-chat-2.png b/public/screenshot-server-features-persistent-chat-2.png
deleted file mode 100644
index 66c9e77..0000000
Binary files a/public/screenshot-server-features-persistent-chat-2.png and /dev/null differ
diff --git a/public/screenshot-server-features-push-1.jpg b/public/screenshot-server-features-push-1.jpg
deleted file mode 100644
index d89af9e..0000000
Binary files a/public/screenshot-server-features-push-1.jpg and /dev/null differ
diff --git a/public/screenshot-server-features-reactions-1.png b/public/screenshot-server-features-reactions-1.png
index 24f9779..da2dba8 100644
Binary files a/public/screenshot-server-features-reactions-1.png and b/public/screenshot-server-features-reactions-1.png differ
diff --git a/public/screenshot-server-features-watch-together-1.png b/public/screenshot-server-features-watch-together-1.png
deleted file mode 100644
index 96b2511..0000000
Binary files a/public/screenshot-server-features-watch-together-1.png and /dev/null differ
diff --git a/public/screenshot-server-wizard-1.png b/public/screenshot-server-wizard-1.png
deleted file mode 100644
index f93f9ea..0000000
Binary files a/public/screenshot-server-wizard-1.png and /dev/null differ
diff --git a/public/screenshot-troubleshooting-debug-logging-1.png b/public/screenshot-troubleshooting-debug-logging-1.png
new file mode 100644
index 0000000..b12b871
Binary files /dev/null and b/public/screenshot-troubleshooting-debug-logging-1.png differ
diff --git a/public/screenshot-users-audio-1.png b/public/screenshot-users-audio-1.png
index f18f8d9..340e843 100644
Binary files a/public/screenshot-users-audio-1.png and b/public/screenshot-users-audio-1.png differ
diff --git a/public/screenshot-users-audio-2.png b/public/screenshot-users-audio-2.png
index e5ebbf3..a91efec 100644
Binary files a/public/screenshot-users-audio-2.png and b/public/screenshot-users-audio-2.png differ
diff --git a/public/screenshot-users-chat-community.png b/public/screenshot-users-chat-community.png
index c7376d4..572bc04 100644
Binary files a/public/screenshot-users-chat-community.png and b/public/screenshot-users-chat-community.png differ
diff --git a/public/screenshot-users-file-sharing-1.png b/public/screenshot-users-file-sharing-1.png
index 4040592..bbb747b 100644
Binary files a/public/screenshot-users-file-sharing-1.png and b/public/screenshot-users-file-sharing-1.png differ
diff --git a/public/screenshot-users-file-sharing-2.png b/public/screenshot-users-file-sharing-2.png
index 22cce21..c7de431 100644
Binary files a/public/screenshot-users-file-sharing-2.png and b/public/screenshot-users-file-sharing-2.png differ
diff --git a/public/screenshot-users-notifications-1.png b/public/screenshot-users-notifications-1.png
new file mode 100644
index 0000000..010f727
Binary files /dev/null and b/public/screenshot-users-notifications-1.png differ
diff --git a/public/screenshot-users-personalization-1.png b/public/screenshot-users-personalization-1.png
deleted file mode 100644
index 117c8e9..0000000
Binary files a/public/screenshot-users-personalization-1.png and /dev/null differ
diff --git a/public/screenshot-users-personalization-2.png b/public/screenshot-users-personalization-2.png
index f3c6ee5..aca1ff7 100644
Binary files a/public/screenshot-users-personalization-2.png and b/public/screenshot-users-personalization-2.png differ
diff --git a/public/screenshot-users-personalization-3.png b/public/screenshot-users-personalization-3.png
index fd6a88a..05bc326 100644
Binary files a/public/screenshot-users-personalization-3.png and b/public/screenshot-users-personalization-3.png differ
diff --git a/public/screenshot-users-privacy-1.png b/public/screenshot-users-privacy-1.png
new file mode 100644
index 0000000..1c31c42
Binary files /dev/null and b/public/screenshot-users-privacy-1.png differ
diff --git a/public/screenshot-users-profile-1.png b/public/screenshot-users-profile-1.png
index 747b930..b85773d 100644
Binary files a/public/screenshot-users-profile-1.png and b/public/screenshot-users-profile-1.png differ
diff --git a/public/screenshot-users-profile-2.png b/public/screenshot-users-profile-2.png
index 95439e4..c5159d3 100644
Binary files a/public/screenshot-users-profile-2.png and b/public/screenshot-users-profile-2.png differ
diff --git a/public/screenshot-users-profile-3.png b/public/screenshot-users-profile-3.png
index 6f13bcd..0ce1250 100644
Binary files a/public/screenshot-users-profile-3.png and b/public/screenshot-users-profile-3.png differ
diff --git a/public/screenshot-users-screen-sharing-1.png b/public/screenshot-users-screen-sharing-1.png
index 8cc80d8..72116fc 100644
Binary files a/public/screenshot-users-screen-sharing-1.png and b/public/screenshot-users-screen-sharing-1.png differ
diff --git a/public/shortcuts.png b/public/shortcuts.png
index 868d9cd..9d7fbc5 100644
Binary files a/public/shortcuts.png and b/public/shortcuts.png differ
diff --git a/src/content/docs/admin/acl.mdx b/src/content/docs/admin/acl.mdx
index 35f2d05..1ef07a3 100644
--- a/src/content/docs/admin/acl.mdx
+++ b/src/content/docs/admin/acl.mdx
@@ -15,9 +15,7 @@ ACLs are powerful, but easy to over-engineer. Most servers do fine
with mostly-roles plus a handful of per-channel ACL tweaks.
-
- Screenshot placeholder: channel ACL tab with three rules and the inheritance indicator.
-
+
## Opening the ACL editor
diff --git a/src/content/docs/admin/bans.mdx b/src/content/docs/admin/bans.mdx
index 9177cb5..1cf7492 100644
--- a/src/content/docs/admin/bans.mdx
+++ b/src/content/docs/admin/bans.mdx
@@ -15,9 +15,7 @@ The **Ban list** is a server-wide block list of:
Banned identities cannot connect. The list survives restarts.
-
- Screenshot placeholder: ban list with five entries and the "Add ban" button.
-
+
## Ban from the user list
@@ -48,25 +46,9 @@ Open **Admin, Ban list**, click **Add ban**:
Useful when you know who you want to block before they connect.
-## Auto-ban on the older C++ server
+## Connection limits
-The settings below apply only to the older C++ server image. They are not Starling configuration. For Starling, consult its [current TOML reference](https://github.com/Fancy-Mumble/starling/blob/main/examples/reference.toml) before configuring connection limits:
-
-```yaml
-environment:
- MUMBLE_CONFIG_AUTOBANATTEMPTS: 5
- MUMBLE_CONFIG_AUTOBANTIMEFRAME: 60
- MUMBLE_CONFIG_AUTOBANTIME: 3600
-```
-
-| Key | What |
-|-----|------|
-| `autobanattempts` | Failed connections per source IP that triggers a ban. |
-| `autobantimeframe` | Window (seconds) in which the attempts are counted. |
-| `autobantime` | How long the auto-ban lasts (seconds). |
-
-Auto-bans show in the ban list with a "auto" tag and can be removed
-manually.
+Use the [Starling configuration reference](https://github.com/Fancy-Mumble/starling/blob/main/examples/reference.toml) for gateway connection and rate limits.
## IP-range bans
diff --git a/src/content/docs/admin/channels.mdx b/src/content/docs/admin/channels.mdx
index 155f73d..aaf8ba7 100644
--- a/src/content/docs/admin/channels.mdx
+++ b/src/content/docs/admin/channels.mdx
@@ -11,9 +11,7 @@ Channels are the rooms in your server. Users sit in one channel at a
time for voice; chat is per channel.
-
- Screenshot placeholder: channel tree on the left with a "Voice" parent and three child channels.
-
+
## Create a channel
@@ -51,9 +49,7 @@ Right-click, **Edit channel**. The dialog has several tabs:
| **Audio** | Bandwidth caps, codec overrides. |
-
- Screenshot placeholder: channel-edit dialog with the ACL tab open.
-
+
## Linking channels
diff --git a/src/content/docs/admin/emotes.mdx b/src/content/docs/admin/emotes.mdx
index 811dcac..1cdbe5b 100644
--- a/src/content/docs/admin/emotes.mdx
+++ b/src/content/docs/admin/emotes.mdx
@@ -15,9 +15,7 @@ This feature requires the [file server](/server/features/file-server/)
to be enabled and the **Manage emotes** permission.
-
- Screenshot placeholder: custom emotes admin tab with five uploaded emotes.
-
+
## Upload an emote
@@ -99,16 +97,7 @@ custom ones.
## Storage and limits
-Stored in the file server at `/data/file-server-storage/emotes/`.
-
-Default limits:
-
-```ini
-plugin.file-server.maxEmoteSizeBytes=1048576
-plugin.file-server.maxEmoteCount=1000
-```
-
-Bump these in your custom INI for very large communities.
+Starling stores emote assets through its file service. Keep that service’s database and object storage in your backups. Upload limits and access permissions depend on the running service configuration; use the [file-storage guide](/server/features/file-server/) and your release’s TOML reference. Legacy plugin INI keys do not configure Starling.
## Pitfalls
diff --git a/src/content/docs/admin/groups.mdx b/src/content/docs/admin/groups.mdx
index 535a0ca..99a680a 100644
--- a/src/content/docs/admin/groups.mdx
+++ b/src/content/docs/admin/groups.mdx
@@ -13,9 +13,7 @@ should use instead. Groups are still useful for **per-channel
membership lists** that should not be promoted to server-wide roles.
-
- Screenshot placeholder: channel groups tab listing three groups and inheritance toggle.
-
+
## Groups vs roles
diff --git a/src/content/docs/admin/marketplace.mdx b/src/content/docs/admin/marketplace.mdx
index 1260737..91a8341 100644
--- a/src/content/docs/admin/marketplace.mdx
+++ b/src/content/docs/admin/marketplace.mdx
@@ -1,115 +1,18 @@
---
title: Plugin Marketplace
-description: Browse and install community plugins for your Fancy Mumble server from the built-in marketplace.
-sidebar:
- order: 11
+description: Browse plugins and check server support before installing them.
---
-import { Steps, Aside, Card } from '@astrojs/starlight/components';
+Open **Settings, Marketplace** to browse the catalogue exposed to your client. Availability depends on the catalogue, network access, and the connected server’s plugin capabilities.
-The **Marketplace** tab in the admin panel connects directly to the
-[Fancy Mumble Plugin Marketplace](https://plugins.fancy-mumble.com/)
-— a curated directory of community plugins you can install on your
-server in a single click.
+
-
-Installing marketplace plugins requires **Fancy Mumble Server 0.4.0
-or newer**. On older servers the install button is hidden and a
-warning banner is shown. You can still browse the catalogue.
-
+## Check compatibility
-
- Screenshot placeholder: Marketplace tab showing a search bar and a grid of plugin cards.
-
+Read a plugin’s description, required server version, capabilities, and installation instructions before enabling it. A catalogue entry does not establish compatibility with your Starling release.
-## Browsing plugins
+Starling has a native and WASM plugin host. Remote installation and lifecycle management are still subject to the host’s implemented admin capabilities. Do not assume that an Install control shown by a client means your deployment supports the operation.
-Open **Admin > Marketplace**. The page loads the most popular plugins
-automatically. Each card shows:
+For a Starling deployment, follow [Server plugins](/server/plugins/using/) to install compatible artifacts and restart the affected service. Check its logs and advertised registry to confirm loading. Keep plugin configuration and data in your backups.
-- Plugin name and author.
-- Short description.
-- Star rating and download count.
-- **Official** badge for first-party plugins maintained by the Fancy
- Mumble project.
-- Capability tags (e.g. `slash-commands`, `modals`).
-
-Type in the search box to filter by name, author, or keyword. Results
-update 300 ms after you stop typing.
-
-## Plugin detail page
-
-Click a card to open the full detail page. It shows:
-
-- Full description, author, and homepage link.
-- **README** rendered from Markdown — documentation written by the
- plugin author.
-- **Version history** table:
- - Version number.
- - Release date.
- - Minimum server version and minimum Fancy Mumble server version
- required.
- - Changelog snippet.
- - **Yanked** badge if a version was pulled by the author (prefer a
- different version).
-- Tags and capability list.
-
-
- Screenshot placeholder: Plugin detail page for "fancy-greeter" showing README and version table.
-
-
-## Installing a plugin
-
-
-
-1. Find the plugin you want in the search results or on the detail page.
-
-2. Click **Install**.
-
-3. The client sends an install request to your server referencing the
- plugin's manifest URL from the marketplace. The server downloads
- and verifies the plugin.
-
-4. A confirmation (or error) banner appears once the server responds.
-
-5. Navigate to **Server Plugins** to confirm the plugin appears in the
- list and toggle it on if it is not already enabled.
-
-
-
-
-The install operation happens server-side. Your client just forwards
-the marketplace metadata; it never downloads the plugin binary itself.
-
-
-## After installing
-
-Once the server loads the plugin, it advertises the plugin's
-**manifest** to connected clients. Each user who connects will be
-prompted to review and grant trust before the plugin's UI surfaces
-appear (slash commands, buttons, modals, etc.). See
-[Plugins](../users/plugins) for what users see.
-
-## Refreshing the list
-
-Click **Refresh** to re-fetch the marketplace index. Useful after
-a new plugin is published or if the page loaded with stale results.
-
-## Developer mode
-
-If your Fancy Mumble preferences are set to **Developer** mode, an
-extra URL selector appears in the toolbar. This lets you point the
-marketplace tab at a local development instance (`http://localhost`)
-instead of the production registry. The selection is persisted in
-your preferences and survives restarts.
-
-
-The local URL override is only visible in Developer mode. It is
-intended for plugin authors testing a local marketplace server.
-
-
-## See also
-
-- [Server Plugins](/admin/server-plugins/) — enable, disable, and uninstall plugins already on your server.
-- [Using plugins (server config)](/server/plugins/using/) — install plugins manually via Docker volume mount without the marketplace.
-- [Developing a plugin](/server/plugins/developing/) — build and publish your own plugin to the marketplace.
+Client plugins that expose interactive surfaces also require the user’s trust. See [Client plugins](/users/plugins/).
diff --git a/src/content/docs/admin/onboarding.mdx b/src/content/docs/admin/onboarding.mdx
index e8e8f38..5444e88 100644
--- a/src/content/docs/admin/onboarding.mdx
+++ b/src/content/docs/admin/onboarding.mdx
@@ -20,9 +20,7 @@ If you have used Discord's "Community" onboarding, this is the same
idea.
-
- Screenshot placeholder: onboarding modal as seen by a new user, with three questions.
-
+
## What you can configure
@@ -66,9 +64,7 @@ idea.
-
- Screenshot placeholder: onboarding admin panel with two questions and four answers each.
-
+
## Example: gaming community
diff --git a/src/content/docs/admin/roles.mdx b/src/content/docs/admin/roles.mdx
index d4d88e5..b3f5ea1 100644
--- a/src/content/docs/admin/roles.mdx
+++ b/src/content/docs/admin/roles.mdx
@@ -13,9 +13,7 @@ everywhere on the server. Roles are the bread-and-butter of
administration.
-
- Screenshot placeholder: roles list with five roles, each showing a colored badge.
-
+
## Built-in roles
@@ -51,9 +49,7 @@ Open the role and switch to the **Members** tab:
- **Bulk add** by pasting a list of usernames.
-
- Screenshot placeholder: role-members tab with five members and an autocomplete on top.
-
+
## Set permissions
@@ -128,9 +124,7 @@ The Display tab of a role lets you tweak:
in the sidebar).
-
- Screenshot placeholder: role display panel with the live badge preview.
-
+
## Audit log
diff --git a/src/content/docs/admin/server-plugins.mdx b/src/content/docs/admin/server-plugins.mdx
index 3f11c1d..8807b23 100644
--- a/src/content/docs/admin/server-plugins.mdx
+++ b/src/content/docs/admin/server-plugins.mdx
@@ -1,10 +1,19 @@
---
title: Server plugins
-description: Understand Starling's plugin host and its current installation path.
+description: Inspect and manage plugins advertised by Starling.
---
-Starling has a plugins service and a Rust plugin host. It can load native plugins and WebAssembly components from its configured plugin directory. The host has been exercised with the published native examples and a WebAssembly component; installing plugin bytes over the wire and operator REST routes remain unfinished in the checked-out implementation.
+Starling runs plugins inside its **plugins** service. Native plugin libraries and WebAssembly components use the host's scoped capabilities for configuration, sessions, channels, permissions, and messaging.
-For now, operators place a compatible plugin binary in the configured plugins directory and enable it through the available plugin RPC. Check its startup logs and advertised plugin registry before expecting client controls to appear. A built-in service named plugins does not mean every plugin from the older C++ server has been ported.
+The service reads its host options from `[services.plugins.options]`. Configure `plugins_dir` to the directory containing compatible plugin artifacts. With no directory configured, no plugins are loaded.
-See the [Starling plugin-host status and porting plan](https://github.com/Fancy-Mumble/starling/blob/main/docs/PLUGIN-HOST-PLAN.md) and [feature availability](/server/features/). The older [plugin guides](/server/plugins/overview/) describe the C++ fork's host and INI configuration.
+~~~toml
+[services.plugins.options]
+plugins_dir = "/var/lib/starling/plugins"
+~~~
+
+Per-plugin settings use the `plugin.
.` prefix inside that options table. Read the plugin's own documentation for its keys. After startup, check the service logs and advertised registry; a plugin file on disk does not prove that it loaded successfully.
+
+The host supports listing, enabling, disabling, and uninstalling loaded plugins. Installation from remotely uploaded bytes and the operator REST plugin routes remain unfinished in the current host plan. Place artifacts through your deployment tooling and use only the administration operations supported by your release.
+
+See the [plugin host implementation and plan](https://github.com/Fancy-Mumble/starling/blob/main/docs/PLUGIN-HOST-PLAN.md), [Starling configuration reference](https://github.com/Fancy-Mumble/starling/blob/main/examples/reference.toml), and [feature availability](/server/features/).
diff --git a/src/content/docs/admin/superuser.mdx b/src/content/docs/admin/superuser.mdx
index 0a87ed6..94d6218 100644
--- a/src/content/docs/admin/superuser.mdx
+++ b/src/content/docs/admin/superuser.mdx
@@ -13,6 +13,6 @@ If you lose the password, stop the server and run the command against the same d
starling set-superuser-password "a-new-strong-password" --server 1 --config starling.toml
~~~
-Omit the server argument for the first configured instance. The older Docker variable MUMBLE_SUPERUSER_PASSWORD and the C++ mumble-server --set-su-pw command do not configure Starling.
+Omit the server argument for the first configured instance.
After logging in, set a welcome text, create channels, configure ACLs and roles, and make a [backup](/server/upgrade/).
diff --git a/src/content/docs/admin/users.mdx b/src/content/docs/admin/users.mdx
index 38dfc6e..3b4ed6c 100644
--- a/src/content/docs/admin/users.mdx
+++ b/src/content/docs/admin/users.mdx
@@ -18,9 +18,7 @@ specific username on your server. Registered users can:
This page covers the **Registered users** tab in the admin panel.
-
- Screenshot placeholder: registered users tab listing eight users with role badges.
-
+
## Register a user
@@ -34,7 +32,7 @@ The user right-clicks their own name in the user list and picks
- **Auto-approves** (default for many servers).
- **Queues for admin approval**.
-Registration policy depends on the server implementation. The older C++ image uses MUMBLE_CONFIG_* variables; they do not configure Starling. Check the current [Starling settings reference](https://github.com/Fancy-Mumble/starling/blob/main/examples/reference.toml) and test the desired approval flow before inviting users.
+Registration policy is controlled by Starling. Check the current [Starling settings reference](https://github.com/Fancy-Mumble/starling/blob/main/examples/reference.toml) and test the desired approval flow before inviting users.
### Admin-initiated
diff --git a/src/content/docs/getting-started/android.mdx b/src/content/docs/getting-started/android.mdx
index a14394e..17a2165 100644
--- a/src/content/docs/getting-started/android.mdx
+++ b/src/content/docs/getting-started/android.mdx
@@ -13,7 +13,7 @@ chat, screen-share viewing, file sharing, custom emotes) and a few
small adjustments for touch.
-
+
## Install
diff --git a/src/content/docs/getting-started/connect.mdx b/src/content/docs/getting-started/connect.mdx
index 38055a7..07e8106 100644
--- a/src/content/docs/getting-started/connect.mdx
+++ b/src/content/docs/getting-started/connect.mdx
@@ -1,37 +1,21 @@
---
title: Connect to a server
-description: Add a saved server, choose an identity, and connect in Nebula or Standard.
+description: Save a server address and choose the identity you will use.
---
-Fancy Mumble can connect to Starling and to standard Mumble servers. Ask the server owner for its host name, port if different from **64738**, and any password.
+Ask the server owner for its host name, port, and any password. The default Mumble port is **64738**.
-## Add a server in Nebula
+1. Choose **Add a server** from the server list.
+2. Enter the address, port, username, and a friendly server label. Choose a certificate identity if you have more than one.
+3. Save the entry, select it in the server list, and choose **Connect**.
+4. Complete any server password or account prompt. The app can remember passwords in the operating system's credential store.
-Nebula is the design for a new profile.
+
-1. Open **Add server** from the server rail or choose **Add server by address…** from **Quick connect**.
-2. Enter the host, port, and the username others should see. The dialog can also save a friendly label, certificate identity, and password.
-3. Save the server. Select it in the server rail, then choose **Connect** on its server screen.
-4. If prompted, enter the server password or account credentials. The app can remember a password in the operating system's credential store.
+The saved entry belongs to the identity you chose. Use your server owner's address; sample addresses in documentation are examples.
-
+## Server certificates
-After saving, the server screen shows the identity and **Connect** action. The example address in these screenshots is illustrative; use the address from your server owner.
+The client remembers the server's TLS certificate after the first connection. If its fingerprint changes, confirm the new fingerprint with the owner before accepting it. Keep your client certificate identity when reconnecting so registered access and profile data continue to match.
-
-
-To find a server instead, choose **Browse public servers** from Quick connect. A server must opt into the public directory to appear there.
-
-## Add a server in Standard
-
-Open the **Connect** screen and use the **Wizard** or **+ Add Server** action. Enter the address and username, save, then select the server card to connect. Standard also has a **Public** view for browsing listed servers. Existing profiles that use Standard keep that design.
-
-## Certificate and identity
-
-The app remembers a server's TLS certificate after the first connection. If its fingerprint changes later, confirm the change with the server owner before accepting it. The owner may have restored a new data directory or replaced the certificate.
-
-A saved certificate identity matters for registration and some Fancy features. Nebula selects an existing default certificate when you add a server, while an existing saved server keeps the identity you chose. For account or server password problems, see [Connection troubleshooting](/troubleshooting/connection/).
-
-## Next step
-
-[Join a channel and make your first call](/getting-started/first-call/).
+For failed logins, see [Connection troubleshooting](/troubleshooting/connection/). Next, [join a channel and make your first call](/getting-started/first-call/).
diff --git a/src/content/docs/getting-started/first-call.mdx b/src/content/docs/getting-started/first-call.mdx
index 0b0d754..fd8c0ad 100644
--- a/src/content/docs/getting-started/first-call.mdx
+++ b/src/content/docs/getting-started/first-call.mdx
@@ -1,118 +1,30 @@
---
title: Your first voice call
-description: Move into a channel, choose between Push-to-Talk and Voice Activation, and verify everyone can hear you.
-sidebar:
- order: 3
+description: Join a channel, enable voice, and choose an activation mode.
---
-import { Steps, Aside, Card, CardGrid } from '@astrojs/starlight/components';
-import { Icon } from 'astro-icon/components';
+After connecting, select a channel in the sidebar and use its **Join** action. Viewing a channel's chat and joining its voice room are separate actions.
-You are connected. Here is how to start **talking**.
+
-
-The channel click sequence and screenshots below describe Standard. Nebula is the default for new profiles; select a channel and use its Join action to enter voice.
-
+## Enable voice
-## 1. Move into a channel
+Use the microphone control in your voice dock. If it says **Enable voice**, the audio engine is off. Open **More, Settings, Voice** to choose the input and output devices, then turn voice on.
-Mumble does not have one big room, channels are explicit. In the left
-sidebar:
+## Choose an activation mode
-- Single-click a channel to **view** it. You will not leave your current
- channel, but you can see who is in the other channel and read its
- chat.
-- Double-click a channel to **join** it. Your avatar moves there, and you
- can talk to the people in that channel.
-- Right-click a channel for the context menu (rename, ACL, link, and
- more).
+The **Activation mode** cards offer **Voice activation**, **Continuous**, and **Push to talk**. Voice activation transmits while you speak; Continuous transmits continuously; Push to talk transmits while your chosen shortcut is held.
-The channel you are in is highlighted, and your name appears beneath
-the channel in the user list.
+
+## Calibrate and listen
-
+Choose **Auto calibrate**, press **Calibrate**, and speak naturally for about five seconds. The gate tunes its threshold, hysteresis, and hold to your microphone. **Manual calibrate** lets you adjust the Open and Close markers yourself.
-## 2. Pick an activation mode
+Use **Hear yourself, Record sample** to record up to twenty seconds through the filters your listeners receive, then play it back. Join a test channel with a friend and confirm that the speaking indicator responds and they can hear the start and end of your sentences.
-Open **Settings, Voice** (or click your avatar then Settings). Three
-modes are available:
+## Mute and deafen
-
-
- Your mic transmits when speech is detected. Threshold based. The
- AI noise removal cleans the result.
-
-
- Audio only flows while a key is held. Pick the key with the
- shortcut recorder. Best for noisy rooms or for podcasters.
-
-
- Always on, no gate, no noise removal. Use only when you need full
- fidelity and the room is silent.
-
-
+Mute stops outgoing audio. Deafen also stops incoming audio. Keep your microphone unmuted when using Push to talk; the shortcut controls transmission.
-
-
-
-## 3. Calibrate
-
-Hit the **Calibrate** button on the Voice panel. A live level meter
-pops up so you can:
-
-- Speak normally. The green fill should comfortably cross the
- threshold line.
-- Stop talking. The fill should drop back below the line.
-
-If your voice barely crosses the line, lower the **Threshold** or
-boost **Microphone Volume**. If the line is constantly below ambient
-noise, raise the threshold.
-
-
-Turn on **Auto Sensitivity** to let the app learn your room
-automatically. It tracks your background noise over the first ten
-seconds or so and adjusts the threshold for you.
-
-
-## 4. Test in a Test channel
-
-Most servers have a *Test* or *Lobby* channel. Join it, hit your
-push-to-talk key (or just talk if you are on Voice Activation), and
-watch the green ring appear around your avatar. That ring is the
-speaking indicator.
-
-You can also watch the local level meter in the header.
-
-## 5. Mute and deafen
-
-Two icons sit at the top of the window:
-
-- Mute outgoing audio.
-- Deafen (mute plus do
- not receive any audio).
-
-Push-to-talk users almost always leave themselves toggled-mute and
-rely on the push-to-talk key instead.
-
-## 6. Adjust on the fly
-
-| If... | Then... |
-|-------|---------|
-| Background noise leaks through | Switch noise removal to **DeepFilterNet**. It is the heaviest and cleanest option. |
-| Voice cuts at the start of sentences | Raise **Hold Frames** in Voice, Expert. |
-| Audio is choppy on Wi-Fi | Lower the bitrate, or enable **Force TCP** in Voice, Network. |
-| You sound quiet | Enable **Auto Gain** in Voice, Audio Processing. |
-
-Full details on the [Audio configuration](/users/audio/) page.
-
-## You are live
-
-That is it, you are now a fully operational Mumble user. Recommended
-next stops:
-
-
-1. Pretty up your account, see [Profile customization](/users/profile/).
-2. Set per-event notification sounds, see [Notifications](/users/notifications/).
-3. Bind a global mute key, see [Keyboard shortcuts](/users/shortcuts/).
-
+If speech cuts out, recalibrate or adjust the gate. If background noise leaks through, try another available noise-suppression algorithm. See [Audio configuration](/users/audio/) for the processing and transmission controls.
diff --git a/src/content/docs/getting-started/install.mdx b/src/content/docs/getting-started/install.mdx
index cea9a12..7f255d6 100644
--- a/src/content/docs/getting-started/install.mdx
+++ b/src/content/docs/getting-started/install.mdx
@@ -11,12 +11,7 @@ import { Icon } from 'astro-icon/components';
Fancy Mumble runs on Windows, Linux, and Android. Pick your platform
below.
-
-New profiles use the Nebula design shown below. Existing Standard profiles can connect to the same servers.
-
-
-
-
+
@@ -119,7 +114,7 @@ name, interface mode, and theme. Choose **Get started** to open the server
list, which is empty until you add a server.
-
+
If you do not see this screen, check
[Common issues](/troubleshooting/common-issues/).
diff --git a/src/content/docs/index.mdx b/src/content/docs/index.mdx
index 872468c..a195926 100644
--- a/src/content/docs/index.mdx
+++ b/src/content/docs/index.mdx
@@ -5,7 +5,7 @@ template: splash
hero:
tagline: A modern, feature-rich voice chat for gaming, podcasts, communities, and study groups.
image:
- html: ' '
+ html: ' '
actions:
- text: Get started in 5 minutes
link: /getting-started/install/
@@ -54,7 +54,7 @@ These docs are still being written. Some pages may be incomplete, missing, or no
[View on GitHub →](https://github.com/Fancy-Mumble/starling)
- Use the maintained Starling Compose deployment or a release package. The older C++ Docker image remains for existing servers.
+ Use the maintained Starling Compose deployment or a release package. Existing servers can follow [Migrating to Starling](/server/migrating-to-starling/).
[Docker quick start →](/server/docker/)
diff --git a/src/content/docs/reference/config-keys.mdx b/src/content/docs/reference/config-keys.mdx
index e247f15..78bf78d 100644
--- a/src/content/docs/reference/config-keys.mdx
+++ b/src/content/docs/reference/config-keys.mdx
@@ -15,4 +15,4 @@ Common keys:
| services.screenshare public_url | Reachable screen-share media endpoint. |
| services.NAME enabled | Explicitly disable a built-in service. |
-For precedence, includes, reload behavior, and the operator API, read the [configuration guide](https://github.com/Fancy-Mumble/starling/blob/main/docs/CONFIGURATION.md). MUMBLE_CONFIG_*, mumble-server.ini, and plugin.* are legacy C++ server settings; use Starling's migrate-config command when moving an existing deployment.
+For precedence, includes, reload behavior, and the operator API, read the [configuration guide](https://github.com/Fancy-Mumble/starling/blob/main/docs/CONFIGURATION.md). See [Migrating to Starling](/server/migrating-to-starling/) when moving an existing deployment.
diff --git a/src/content/docs/reference/ports.mdx b/src/content/docs/reference/ports.mdx
index 6ece2a0..bad3517 100644
--- a/src/content/docs/reference/ports.mdx
+++ b/src/content/docs/reference/ports.mdx
@@ -11,5 +11,3 @@ description: Listener reference for the maintained Starling Compose deployment.
| 8081 | TCP | Optional operator API; the Compose admin profile binds it to localhost. |
The first two are needed for a normal public server. The other ports and service listeners depend on the deployment and its TOML. A configured screen-share relay needs a separately reachable UDP media port. Internal gRPC endpoints should remain private. See [Ports and networking](/server/network/) and the [Starling Compose file](https://github.com/Fancy-Mumble/starling/blob/main/docker-compose.yml).
-
-The older C++ server image uses different optional ports, including 64739 for its file plugin, 64740 for live documents, 10000/UDP for screen sharing, and 6502 for Ice. Those are not Starling defaults.
diff --git a/src/content/docs/server/config.mdx b/src/content/docs/server/config.mdx
index 7d2b898..f23bd0d 100644
--- a/src/content/docs/server/config.mdx
+++ b/src/content/docs/server/config.mdx
@@ -3,7 +3,7 @@ title: Starling configuration
description: Configure the current server with a small TOML overlay and live instance settings.
---
-Starling reads starling.toml. A file overlays built-in defaults: omitted keys keep their defaults, while unknown keys cause startup to fail. The old MUMBLE_CONFIG_* variables and mumble-server.ini apply to the legacy C++ image, not Starling.
+Starling reads starling.toml. A file overlays built-in defaults: omitted keys keep their defaults, while unknown keys cause startup to fail. For an existing deployment, use [Migrating to Starling](/server/migrating-to-starling/).
## A small server
@@ -26,4 +26,4 @@ Settings under the instances.settings table, such as max_users, password, and we
Services use services.NAME tables. To turn a built-in service off, set enabled = false; leaving a table out keeps its default. The operator API is an exception: it is absent until explicitly configured. Files use the services.files table with an HTTP listen address and a client-reachable public_url. Screen-share relay needs a reachable media address. See the [configuration guide](https://github.com/Fancy-Mumble/starling/blob/main/docs/CONFIGURATION.md) and [reference TOML](https://github.com/Fancy-Mumble/starling/blob/main/examples/reference.toml) for the full schema.
-Starling can reload supported settings on SIGHUP. Listener changes can require a restart; check the reload log for applied, next_connection, and pending_restart. For an existing C++ server, use the Starling migrate-config command to produce a starting TOML overlay, then review it before starting Starling.
+Starling can reload supported settings on SIGHUP. Listener changes can require a restart; check the reload log for applied, next_connection, and pending_restart. See [Migrating to Starling](/server/migrating-to-starling/) for configuration conversion and database import.
diff --git a/src/content/docs/server/customize.mdx b/src/content/docs/server/customize.mdx
index 83407e5..b59672b 100644
--- a/src/content/docs/server/customize.mdx
+++ b/src/content/docs/server/customize.mdx
@@ -21,4 +21,4 @@ enabled = false
Omitting a service table keeps its built-in default. To switch a service off, set enabled = false. The operator API is disabled unless explicitly configured. Per-channel access is managed through ACLs in the client; for example, file sharing requires the corresponding share permission in that channel.
-Check the [Starling configuration guide](https://github.com/Fancy-Mumble/starling/blob/main/docs/CONFIGURATION.md) and [reference TOML](https://github.com/Fancy-Mumble/starling/blob/main/examples/reference.toml) for exact keys and defaults. Old MUMBLE_CONFIG_* and plugin.* INI examples refer to the C++ server and cannot be copied into Starling TOML.
+Check the [Starling configuration guide](https://github.com/Fancy-Mumble/starling/blob/main/docs/CONFIGURATION.md) and [reference TOML](https://github.com/Fancy-Mumble/starling/blob/main/examples/reference.toml) for exact keys and defaults. See [Migrating to Starling](/server/migrating-to-starling/) when moving an existing deployment.
diff --git a/src/content/docs/server/docker.mdx b/src/content/docs/server/docker.mdx
index e7c75a0..2459080 100644
--- a/src/content/docs/server/docker.mdx
+++ b/src/content/docs/server/docker.mdx
@@ -3,7 +3,7 @@ title: Run Starling with Docker
description: Start the current Fancy Mumble server using Starling's maintained Compose deployment.
---
-Starling is the current Fancy Mumble server. Its repository contains the Compose file and the matching configuration file. The older mumble-docker image runs the C++ server fork and is retained for existing deployments.
+Starling is the current Fancy Mumble server. Its repository contains the Compose file and the matching configuration file. For an existing deployment, follow [Migrating to Starling](/server/migrating-to-starling/).
## Start a server
diff --git a/src/content/docs/server/features/file-server.mdx b/src/content/docs/server/features/file-server.mdx
index e4ca4a9..8562400 100644
--- a/src/content/docs/server/features/file-server.mdx
+++ b/src/content/docs/server/features/file-server.mdx
@@ -1,267 +1,25 @@
---
-title: File server
-description: Built-in file storage for custom emotes, avatars, and chat attachments.
-sidebar:
- order: 4
+title: File sharing on Starling
+description: Configure file sharing and the addresses clients use for uploads and downloads.
---
-import { Steps, Aside, Tabs, TabItem, Card, CardGrid, FileTree } from '@astrojs/starlight/components';
-import PlantUML from '../../../../components/PlantUML.astro';
-import { MUMBLE_DOCKER_BRANCH } from '../../../../lib/github';
+Starling's **files** service owns file metadata, access checks, and signed URLs. Keep it enabled and give clients a reachable public URL. The shipped Compose deployment supplies the matching HTTP path; a custom deployment must supply its file delivery endpoint.
-:::caution[Legacy C++ server instructions]
-This page documents the older mumble-server fork. Its INI keys, plugin paths, and ports do not configure Starling. For a new deployment, start with [Starling feature availability](/server/features/) and the [Starling configuration guide](/server/config/).
-:::
+## Configure the endpoint
-The file server is the part of Fancy Mumble that handles:
+Set `public_url` under `[services.files]` to the address users can reach. A loopback address works only for clients on the server machine. Use HTTPS when exposing the endpoint publicly, and keep the reverse proxy's upload limit compatible with `max_upload`.
-- **Custom server emote** images.
-- **Chat file attachments** (the share dialog).
+~~~toml
+[services.files]
+public_url = "https://files.example.com"
+max_upload = "512MiB"
+url_ttl = "15m"
+~~~
-It is an HTTP service that listens on **port 64739** by default. The
-listener speaks **plain HTTP** - TLS is **not built in**. On a private
-network that is fine; on the public internet, put it behind a
-TLS-terminating reverse proxy (see
-[Behind a reverse proxy](#behind-a-reverse-proxy) below) so that
-clients reach it as **HTTPS**.
+Use the [Starling configuration reference](https://github.com/Fancy-Mumble/starling/blob/main/examples/reference.toml) for the complete schema and the [Compose deployment](/server/docker/) for a working stack.
-Without it, file sharing and custom emotes are unavailable, the
-buttons are hidden in the app.
+## Access and testing
-
-- **Avatars** are stored inside the main server database
- (`mumble-server.sqlite`) as a user texture, not on the file
- server. They survive backup and restore as part of the database.
-- **Link preview images** are sent inline in the link-preview
- response message (as a base64-encoded payload, see
- [Link previews](/server/features/link-previews/)). They are not
- stored on the file server.
-
+Channel permissions determine which sharing modes a user can choose. Test uploads and downloads from a second client outside the server's network. A successful upload on localhost does not prove that another member can reach its download URL.
- RP : HTTPS 443
-Web --> RP : HTTPS 443
-RP --> FS : HTTP 64739 (behind proxy)
-App ..> FS : HTTP 64739 (direct, LAN only)
-Web ..> FS : HTTP 64739 (direct, LAN only)
-
-note bottom of FS
- No built-in TLS.
- Skip the proxy only on
- a trusted private network.
-end note
-@enduml
-`} />
-
-## Turn it on
-
-The Docker image ships with the file server already enabled. The
-defaults are:
-
-```ini
-plugin.file-server.enabled=true
-plugin.file-server.storagePath=/data/file-server-storage
-plugin.file-server.bindAddress=0.0.0.0
-plugin.file-server.port=64739
-plugin.file-server.tlsTerminatedByProxy=true
-; plugin.file-server.baseUrl=https://your-domain.example/files
-; plugin.file-server.allowedOrigins=https://your-domain.example
-```
-
-### Where does the config file live?
-
-On a fresh container started with the official image, the config
-file is generated at boot under:
-
-```text
-/data/mumble_server_config.ini
-```
-
-This is the file the entrypoint script writes whenever you change a
-`MUMBLE_CONFIG_*` environment variable. It is regenerated on every
-container start, so editing it directly does **not** survive a
-restart unless you also set `MUMBLE_CUSTOM_CONFIG_FILE` to point at
-a file you own.
-
-To set `plugin.*` keys for the first time:
-
-
-1. Copy the sample config out of the image to your host:
-
- ```bash
- docker cp mumble-server:/data/mumble_server_config.ini ./mumble-server.ini
- ```
-
- Or grab the template from the Docker repo at
- `mumble-server.ini.example` .
-
-2. Edit the `plugin.file-server.*` keys to your taste.
-
-3. Mount the file back into the container and tell the entrypoint
- to use it instead of the env-var-generated one:
-
- ```yaml
- environment:
- MUMBLE_CUSTOM_CONFIG_FILE: /data/mumble-server.ini
- volumes:
- - ./mumble-server.ini:/data/mumble-server.ini:ro
- - mumble-data:/data
- ```
-
-4. Restart the container:
-
- ```bash
- docker compose up -d --force-recreate mumble-server
- ```
-
-
-
-Once `MUMBLE_CUSTOM_CONFIG_FILE` is set, **all `MUMBLE_CONFIG_*`
-environment variables are ignored**. Put everything (file-server
-keys plus the regular voice and chat settings) in the mounted INI.
-
-
-| Key | Default | Description |
-|-----|---------|-------------|
-| `plugin.file-server.enabled` | `true` | Master toggle. |
-| `plugin.file-server.storagePath` | `/data/file-server-storage` | Where files live on disk. Must be absolute. |
-| `plugin.file-server.bindAddress` | `127.0.0.1` (with safer default in the image) | Address to listen on. Use `0.0.0.0` to expose directly. |
-| `plugin.file-server.port` | `64739` | TCP port. |
-| `plugin.file-server.tlsTerminatedByProxy` | `false` | Set to `true` when a reverse proxy handles TLS. |
-| `plugin.file-server.baseUrl` | unset | Public URL clients use. Falls back to `http://host:port` when empty. |
-| `plugin.file-server.allowedOrigins` | unset | Comma-separated CORS origins. Empty blocks browser usage. |
-| `plugin.file-server.maxUploadSizeBytes` | `52428800` (50 MB) | Per-file upload limit. |
-| `plugin.file-server.retentionDays` | `90` | How long public files live. |
-
-## Expose the port
-
-In `docker-compose.yml`:
-
-```yaml
-ports:
- - "64739:64739/tcp"
-```
-
-Without this port exposed externally, the file server is only
-reachable from inside the container. Useful if you put a reverse
-proxy in front and only expose the proxy.
-
-## Behind a reverse proxy
-
-Recommended for any deployment reachable from the public internet.
-The proxy accepts **HTTPS** from clients and forwards it as plain
-**HTTP** to the file server. Tell the file server to trust the proxy
-headers:
-
-```ini
-plugin.file-server.tlsTerminatedByProxy=true
-plugin.file-server.baseUrl=https://files.example.com
-plugin.file-server.allowedOrigins=https://files.example.com
-```
-
-A minimal nginx config:
-
-```nginx
-server {
- listen 443 ssl http2;
- server_name files.example.com;
-
- ssl_certificate /etc/letsencrypt/live/files.example.com/fullchain.pem;
- ssl_certificate_key /etc/letsencrypt/live/files.example.com/privkey.pem;
-
- client_max_body_size 100m;
-
- location / {
- proxy_pass http://mumble-server:64739;
- proxy_set_header Host $host;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto $scheme;
- }
-}
-```
-
-## Storage layout
-
-
-- /data/file-server-storage/
- - emotes/ custom server emotes, keyed by shortcode
- - attachments/ chat attachments
- - public/ public-mode uploads
- - password/ password-protected uploads
- - session/ session-only uploads (cleaned up)
- - index.sqlite metadata, not file data
-
-
-Back up the whole `file-server-storage` folder along with the
-`mumble-server.sqlite` database for a complete backup.
-
-## Access modes (recap)
-
-When a user uploads a file, they pick one of three modes. See
-[Files and images](/users/file-sharing/) for the user view.
-
-| Mode | Who can download | Lifetime |
-|------|------------------|----------|
-| Public | Anyone with the link | `retentionDays` |
-| Password | Anyone with the link AND the password | `retentionDays` |
-| Session | Only currently connected users | A short while after every recipient disconnects |
-
-You can restrict who is allowed to use public/password modes per
-role. See [Permission flags](/reference/permissions/).
-
-## Disk usage
-
-For a 50-user server, plan for roughly:
-
-- **Avatars**: 100 KB per user. Negligible.
-- **Custom emotes**: 50 KB per emote, often a few hundred per server.
-- **Attachments**: this is the variable. A busy media-sharing server
- can use 10 GB or more per month. Cap with `maxUploadSizeBytes` and
- `retentionDays`.
-
-Add monitoring on `/data/file-server-storage` to catch surprises.
-
-## Disabling
-
-```ini
-plugin.file-server.enabled=false
-```
-
-The client UI hides the file-attach controls when no
-`fancy-file-server-config` message is received. Existing avatars
-and emotes still work as long as the storage directory and database
-are intact.
-
-## Pitfalls
-
-- **Paperclip is missing in chat**: the file server is not enabled, or
- the client cannot reach the port. Check
- `curl -I http://server:64739/healthz` from a client machine.
-- **CORS errors in browser builds**: set `allowedOrigins` to your web
- client's URL.
-- **Uploads fail with 413**: `maxUploadSizeBytes` is too small, or
- your reverse proxy's `client_max_body_size` is.
-- **Files stay forever**: `retentionDays=0` is "forever". Set to a
- positive number for cleanup.
-
-## Next step
-
-Continue with [Link previews](/server/features/link-previews/).
+See [Files and images](/users/file-sharing/) for the client workflow. Back up the file storage together with its service database; a database backup alone does not preserve uploaded bytes.
diff --git a/src/content/docs/server/features/index.mdx b/src/content/docs/server/features/index.mdx
index 9ffd07c..b66983c 100644
--- a/src/content/docs/server/features/index.mdx
+++ b/src/content/docs/server/features/index.mdx
@@ -18,6 +18,6 @@ Starling is the current server. Its gateway routes client control traffic to sep
| Live documents and calendar | Plugin-dependent | The checked-out Starling porting plan still lists these plugins as pending migration. Do not assume they work in a fresh deployment. |
| External administration | operator-api | Explicitly configure authentication and the admin profile. |
-Some client features also depend on a plugin, server capability, or a newer client build. If a control is absent after connecting, check the server's advertised capabilities and its logs. The older feature and plugin guides remain available for existing C++ deployments; their INI keys and ports are not Starling configuration recipes.
+Some client features also depend on a plugin, server capability, or a newer client build. If a control is absent after connecting, check the server's advertised capabilities and its logs.
For the current plugin-host implementation and remaining migration work, see the [Starling plugin-host plan](https://github.com/Fancy-Mumble/starling/blob/main/docs/PLUGIN-HOST-PLAN.md). For each feature's exact settings, use the [Starling TOML reference](https://github.com/Fancy-Mumble/starling/blob/main/examples/reference.toml).
diff --git a/src/content/docs/server/features/link-previews.mdx b/src/content/docs/server/features/link-previews.mdx
index 99fb0f3..293e4ff 100644
--- a/src/content/docs/server/features/link-previews.mdx
+++ b/src/content/docs/server/features/link-previews.mdx
@@ -1,80 +1,28 @@
---
title: Link previews
-description: Server-rendered preview cards for URLs in chat.
-sidebar:
- order: 5
+description: Bounded server-side previews for links shared in chat.
---
-import { Aside, Card, CardGrid } from '@astrojs/starlight/components';
+Starling's **link-preview** service fetches page metadata and preview pictures for URLs shared in chat. The resulting card is sent to clients, so members receive the same preview and do not each fetch its image from the origin.
-:::caution[Legacy C++ server instructions]
-This page documents the older mumble-server fork. Its INI keys, plugin paths, and ports do not configure Starling. For a new deployment, start with [Starling feature availability](/server/features/) and the [Starling configuration guide](/server/config/).
-:::
+## Configure preview fetching
-When a user pastes a URL in chat, the server can fetch the page, pull
-out the Open Graph tags, and send a preview card back. Other Fancy
-Mumble clients render the card inline.
+The service is part of the default stack. Its options live under `[services.link-preview.options]`. Values in this options table are strings.
-Rendering on the server has two advantages:
+~~~toml
+[services.link-preview.options]
+preview_timeout_ms = "5000"
+preview_max_bytes = "1048576"
+preview_redirects = "3"
+preview_concurrency = "8"
+preview_image_max_bytes = "2097152"
+preview_image_edge = "640"
+~~~
-- Every client sees the same preview, even if they cannot reach the
- target site.
-- The target site only sees one fetch per shared link, not one per
- client.
+These limits bound page fetches, redirects, parallel work, and thumbnail size. Setting `preview_image_max_bytes = "0"` disables preview pictures. Read the [Starling configuration reference](https://github.com/Fancy-Mumble/starling/blob/main/examples/reference.toml) before changing the limits.
+## Privacy and troubleshooting
-
+The destination site sees a request from your server. Outbound DNS, firewall, and proxy rules can prevent a preview from loading. Check the link-preview service logs, then test a public page with Open Graph metadata. A page without suitable metadata may produce only a text link.
-## How it works
-
-When a Fancy Mumble client posts a chat message that contains a URL,
-it sends a `FancyLinkPreviewRequest` to the server. The server:
-
-1. Validates the URL is safe to fetch (rejects private IP ranges to
- avoid Server-Side Request Forgery).
-2. Rate-limits the request per session.
-3. Fetches the page.
-4. Parses the HTML for Open Graph tags (`og:title`, `og:image`,
- `og:description`, plus `twitter:` fallbacks).
-5. Returns a `FancyLinkPreviewResponse` containing the metadata and
- the preview image encoded inline as base64 bytes.
-
-The receiving Fancy clients render the card. Preview images travel
-inside the protobuf message, **not** through the file server.
-
-## Privacy
-
-Link previewing means the server fetches URLs that users paste.
-Consider:
-
-- The server's IP is logged by the target site.
-- A user who can post in chat can trigger arbitrary outbound fetches.
-
-The server refuses to fetch RFC 1918 (private) IP ranges by default,
-which mitigates the most obvious abuse.
-
-
-The validator rejects `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`,
-and `127.0.0.0/8` to avoid Server-Side Request Forgery against
-internal services on the host network.
-
-
-## Configuration
-
-There are no documented server `MUMBLE_CONFIG_*` keys or `plugin.*`
-keys for link previews in the example config. The behavior is
-controlled in-code. If you need to disable it for your deployment,
-contact the maintainers or build a server image without the
-`LinkPreviewPlugin` compiled in.
-
-## Pitfalls
-
-- **No previews ever**: confirm the server can reach the public
- internet. Try `docker exec mumble-server curl -sI https://example.com`.
-- **A specific site never previews**: it might return JavaScript-
- rendered Open Graph that the server cannot parse, or it might
- block the server's user-agent.
-
-## Next step
-
-Continue with [Reactions & polls](/server/features/reactions/).
+
diff --git a/src/content/docs/server/features/live-doc.mdx b/src/content/docs/server/features/live-doc.mdx
index e61a526..3adae78 100644
--- a/src/content/docs/server/features/live-doc.mdx
+++ b/src/content/docs/server/features/live-doc.mdx
@@ -1,276 +1,10 @@
---
-title: Live Documents
-description: Real-time collaborative document editing inside a channel, backed by a WebSocket-based Yjs CRDT server plugin.
-sidebar:
- order: 9
+title: Live documents
+description: Availability and deployment requirements for collaborative documents.
---
-import { Steps, Aside, Tabs, TabItem, Card, CardGrid, Code } from '@astrojs/starlight/components';
-import PlantUML from '../../../../components/PlantUML.astro';
+The client contains a collaborative document editor, but a server must advertise a compatible live-document plugin before the feature can work. A running Starling plugins service alone does not provide that plugin.
-:::caution[Legacy C++ server instructions]
-This page documents the older mumble-server fork. Its INI keys, plugin paths, and ports do not configure Starling. For a new deployment, start with [Starling feature availability](/server/features/) and the [Starling configuration guide](/server/config/).
-:::
+The current [plugin host plan](https://github.com/Fancy-Mumble/starling/blob/main/docs/PLUGIN-HOST-PLAN.md) lists live-doc among the plugins still requiring a compatibility pass. Verify the plugin against the Starling release you deploy before inviting users to rely on it.
-The **live-doc plugin** lets every member of a channel co-edit a single
-shared document in real time. Documents support rich formatting,
-images with float modes, tables, math (LaTeX), code blocks, and
-collaborative cursors showing where each participant is typing.
-
-Content is synchronized using **Yjs** - a conflict-free replicated data
-type (CRDT) - transported over a dedicated WebSocket port. Changes made
-while offline merge automatically when the client reconnects.
-
-## How it fits together
-
- Server : FancyLiveDocOpen (wire 141)
-Server --> Plugin : FFI callback
-Plugin --> Server : FancyLiveDocInvite (token + ws_url)
-Server --> App : FancyLiveDocInvite
-Server --> App : FancyLiveDocAnnounce (broadcast to channel)
-App --> WS : WebSocket (Yjs sync)
-WS --> Plugin : CRDT updates
-Plugin --> FS : PUT /admin/documents/{name} (snapshot)
-
-note bottom of WS
- Standard y-protocols frames:
- sync step 1 / 2 / update /
- awareness query & update.
-end note
-@enduml
-`} />
-
-1. The client sends a `FancyLiveDocOpen` plugin-data message (wire ID 141).
-2. The server passes it to the live-doc plugin via FFI.
-3. The plugin mints a short-lived JWT and replies with `FancyLiveDocInvite`
- containing the token and the WebSocket URL.
-4. The server broadcasts `FancyLiveDocAnnounce` to the channel so every
- member can join.
-5. All participants connect to the WebSocket endpoint and exchange Yjs
- CRDT frames directly.
-6. After a configurable idle window the plugin flushes a binary snapshot
- to the file server for persistence across restarts.
-
-## Prerequisites
-
-The live-doc plugin depends on the **file server plugin** for persistence.
-Without it, documents are kept in memory only and lost whenever the
-container restarts or the teardown grace window expires.
-
-See [File server](/server/features/file-server/) for setup instructions.
-
-## Turn it on
-
-
-The live-doc plugin is **not enabled** by default. It requires an explicit
-`state_path` directory to store its JWT signing secret and room state.
-Without it the plugin will refuse to start.
-
-
-The plugin is configured only via a mounted INI file - the `plugin.live-doc.*`
-keys are not mapped to `MUMBLE_CONFIG_*` environment variables because they
-require absolute paths that vary per deployment.
-
-
-
-1. If you do not already have a custom config file, copy the template
- from the image:
-
- ```bash
- docker cp mumble-server:/data/mumble_server_config.ini ./mumble-server.ini
- ```
-
-2. Add the following block to your `mumble-server.ini`, adjusting paths
- and URLs for your deployment:
-
- ```ini
- ; Master toggle. Default: false.
- plugin.live-doc.enabled=true
-
- ; Required: writable directory for JWT secret and room state.
- plugin.live-doc.state_path=/data/live-doc-state
-
- ; WebSocket bind address and port. Default: 0.0.0.0:64740.
- ;plugin.live-doc.host=0.0.0.0
- ;plugin.live-doc.port=64740
-
- ; Public base URL for clients (use wss:// when behind a TLS proxy).
- ;plugin.live-doc.public_url=wss://chat.example.com/live-doc
-
- ; Persistence bridge. Must match file-server admin token.
- plugin.live-doc.file_server_url=http://127.0.0.1:64739
- plugin.live-doc.file_server_admin_token=your-shared-secret
- ```
-
-3. Mount the file and create the state directory:
-
- ```yaml
- environment:
- MUMBLE_CUSTOM_CONFIG_FILE: /data/mumble-server.ini
- volumes:
- - ./mumble-server.ini:/data/mumble-server.ini:ro
- - mumble-data:/data # live-doc-state lives inside here
- ```
-
-4. Expose the WebSocket port (default 64740) in `docker-compose.yml`:
-
- ```yaml
- ports:
- - "64738:64738/tcp"
- - "64738:64738/udp"
- - "64739:64739/tcp"
- - "64740:64740/tcp" # live-doc WebSocket
- ```
-
-5. Restart:
-
- ```bash
- docker compose up -d --force-recreate mumble-server
- ```
-
-
-
-
-Once `MUMBLE_CUSTOM_CONFIG_FILE` is set, **all `MUMBLE_CONFIG_*` environment
-variables are ignored**. Move every setting - voice, chat, file server, and
-live-doc - into the one mounted INI file.
-
-
-## Configuration reference
-
-| Key | Default | Description |
-|-----|---------|-------------|
-| `plugin.live-doc.enabled` | `false` | Master toggle. |
-| `plugin.live-doc.state_path` | - | **Required.** Absolute path to a writable directory. The plugin stores its JWT signing secret here. |
-| `plugin.live-doc.host` | `0.0.0.0` | WebSocket bind address. |
-| `plugin.live-doc.port` | `64740` | WebSocket TCP port. |
-| `plugin.live-doc.public_url` | unset | URL clients use to connect (e.g. `wss://chat.example.com/live-doc`). Falls back to `ws://:` when empty. |
-| `plugin.live-doc.max_update_bytes` | `4194304` (4 MiB) | Maximum size of a single CRDT update. |
-| `plugin.live-doc.snapshot_idle_secs` | `60` | Seconds of inactivity before the in-memory document is flushed to the file server. |
-| `plugin.live-doc.teardown_grace_secs` | `30` | Seconds after the last viewer disconnects before the in-memory room is torn down. |
-| `plugin.live-doc.file_server_url` | unset | Base URL of the file server admin endpoint (e.g. `http://127.0.0.1:64739`). When unset, documents are never persisted. |
-| `plugin.live-doc.file_server_admin_token` | unset | Shared secret for the file server admin API. Required when `file_server_url` is set. |
-
-## Behind a reverse proxy
-
-Like the file server, the live-doc WebSocket should be TLS-terminated by a
-reverse proxy on public deployments.
-
-
-The live-doc axum server serves its routes at `/ws/…` (no `/live-doc` prefix).
-Because `public_url` tells the client to connect to
-`wss://chat.example.com/live-doc/ws/…`, the proxy **must strip the
-`/live-doc` prefix** before forwarding to port 64740, otherwise the backend
-returns 404.
-
-
-
-
- `handle_path` strips the matched prefix automatically:
- ```text
- chat.example.com {
- handle_path /live-doc/* {
- reverse_proxy localhost:64740
- }
- }
- ```
- Then set `plugin.live-doc.public_url=wss://chat.example.com/live-doc`.
-
-
- The trailing slash on both `location` and `proxy_pass` strips the prefix:
- ```nginx
- location /live-doc/ {
- proxy_pass http://127.0.0.1:64740/;
- proxy_http_version 1.1;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection "Upgrade";
- proxy_set_header Host $host;
- }
- ```
- Then set `plugin.live-doc.public_url=wss://chat.example.com/live-doc`.
-
-
- Add a `StripPrefix` middleware so Traefik removes `/live-doc` before
- forwarding:
- ```yaml
- http:
- middlewares:
- live-doc-strip:
- stripPrefix:
- prefixes:
- - /live-doc
- routers:
- live-doc:
- rule: "Host(`chat.example.com`) && PathPrefix(`/live-doc`)"
- service: live-doc
- entryPoints: [websecure]
- middlewares:
- - live-doc-strip
- services:
- live-doc:
- loadBalancer:
- servers:
- - url: "http://127.0.0.1:64740"
- ```
- Then set `plugin.live-doc.public_url=wss://chat.example.com/live-doc`.
-
-
-
-
-WebSocket proxying requires `Upgrade` and `Connection` headers to be
-forwarded. Missing them produces an HTTP 400 on connect. Caddy and Traefik
-forward them automatically; nginx requires the explicit `proxy_set_header`
-lines shown above.
-
-
-## Persistence
-
-When both `file_server_url` and `file_server_admin_token` are set, the
-plugin automatically:
-
-- Flushes a binary CRDT snapshot to `PUT /admin/documents/{name}` every
- `snapshot_idle_secs` seconds after the last update.
-- Flushes once more when the last viewer disconnects and the teardown
- grace window expires.
-- Loads the latest saved snapshot when the first viewer opens the document.
-
-Documents that have never been persisted start empty for every viewer.
-
-## Pitfalls
-
-- **Port 64740 not exposed**: the client will time out waiting for the
- WebSocket invite reply. Check `docker compose ps` and confirm the port
- mapping.
-- **`state_path` missing or not writable**: the plugin logs an error and
- refuses to start. The client will never receive a `FancyLiveDocInvite`.
-- **File server admin token mismatch**: snapshots fail silently (logged
- server-side). Documents open and sync normally but are lost on restart.
-- **`MUMBLE_CUSTOM_CONFIG_FILE` not set**: the plugin keys are read from a
- mounted INI but the entrypoint-generated config does not include them.
- Confirm `MUMBLE_CUSTOM_CONFIG_FILE` points at your file.
-
-## Next step
-
-You have walked through all of the feature deep-dives. Continue with
-[Customize & disable features](/server/customize/) for the consolidated
-toggle reference.
+A plugin that exposes an additional HTTP or WebSocket endpoint must advertise an address clients can reach. Its storage and endpoint settings belong to that plugin. See [Server plugins](/server/plugins/overview/) and [Live document editor](/users/live-doc/).
diff --git a/src/content/docs/server/features/persistent-chat.mdx b/src/content/docs/server/features/persistent-chat.mdx
index 8ab1f23..7d64b5f 100644
--- a/src/content/docs/server/features/persistent-chat.mdx
+++ b/src/content/docs/server/features/persistent-chat.mdx
@@ -1,290 +1,24 @@
---
title: Persistent chat
-description: Server-stored encrypted chat history with per-channel retention and ACLs. Choose Full Archive for searchable history or Signal V1 for zero server-side storage.
-sidebar:
- order: 1
+description: Encrypted channel history and retention on Starling.
---
-import { Steps, Aside, Tabs, TabItem, Card, CardGrid, Badge } from '@astrojs/starlight/components';
+Starling's **pchat** service handles persistent channel messages, history fetches, key exchange, and pins. The server stores encrypted payloads; the client performs the end-to-end encryption.
-:::caution[Legacy C++ server instructions]
-This page documents the older mumble-server fork. Its INI keys, plugin paths, and ports do not configure Starling. For a new deployment, start with [Starling feature availability](/server/features/) and the [Starling configuration guide](/server/config/).
-:::
+## Channel policy
-Persistent chat means **messages survive a disconnect**. Without it, a
-Mumble server forgets everything as soon as a user leaves, just like
-a phone call.
+A channel's persistence policy determines whether history is retained. Review its history and encryption controls from the channel settings, and test the policy with two registered identities before using it for a community.
-Fancy Mumble does not implement this as a single scheme. The server
-provides the storage, key-distribution, and rate-limiting machinery,
-and **each channel picks one of three modes** that decide *how*
-encryption and history work. The modes range from "channel forgets
-everything" to "server stores the ciphertext" to "end-to-end
-encrypted via Signal Sender Keys".
+History access follows channel permissions. Registration, encryption mode, retention, and limits affect whether a member can retrieve and decrypt earlier messages. A connected client may already have a local copy even after server retention removes a message.
+## Storage and recovery
-
+The pchat database is owned by that service. Keep its database and the Starling data directory in your backup. The at-rest key is generated in the data directory; preserve it with the encrypted database. Use [Starling configuration reference](https://github.com/Fancy-Mumble/starling/blob/main/examples/reference.toml) and the [storage guide](https://github.com/Fancy-Mumble/starling/blob/main/docs/STORAGE.md) for deployment settings.
-
-- The **server-wide `pchat*` keys** (set via `MUMBLE_CONFIG_*` env
- vars, or in `mumble-server.ini`) only enable the feature and set
- defaults / safety limits.
-- The actual behaviour you experience in a channel is decided by the
- **per-channel `pchat_protocol`**, which is set from the admin UI.
- A brand-new channel has no protocol (`NONE`) and stores nothing
- until an admin picks one.
-
+## Verify the result
-## Server-wide settings
+Send a message, disconnect, reconnect with the same identity, and confirm that history loads. Repeat with another identity that has channel access. Check the pchat logs if messages load but cannot be decrypted.
-These belong in the `environment:` block of your `docker-compose.yml`
-(or in `[server]` of `mumble-server.ini`). They turn the feature on
-and set defaults; they do **not** pick a per-channel protocol.
+Moving an existing deployment has separate history limitations. Read [Migrating to Starling](/server/migrating-to-starling/) before importing its database.
-```yaml
-environment:
- MUMBLE_CONFIG_PCHATENABLED: true
- MUMBLE_CONFIG_PCHATREQUIREREGISTRATION: false
- MUMBLE_CONFIG_PCHATDEFAULTMAXHISTORY: 5000
- MUMBLE_CONFIG_PCHATDEFAULTRETENTIONDAYS: 90
- MUMBLE_CONFIG_PCHATMAXPAYLOADSIZE: 1048576
-```
-
-| Key | Default | Description |
-|-----|---------|-------------|
-| `pchatenabled` | `true` | Master toggle for the persistent-chat subsystem. If `false`, the server rejects every `Pchat*` message regardless of channel protocol. |
-| `pchatrequireregistration` | `false` | If `true`, only registered users can post into a persistent channel. |
-| `pchatdefaultmaxhistory` | `5000` | Default cap on stored messages per channel. Used when a channel is created; admins can override per channel. |
-| `pchatdefaultretentiondays` | `90` | Default age cap in days. `0` means "keep forever". Override per channel. |
-| `pchatmaxpayloadsize` | `1048576` | Max bytes per encrypted envelope (1 MB). Image attachments are base64-encoded inside the envelope, so size this for the largest inline attachment you want to allow. |
-| `pchatpendingkeyrequestmaxdays` | `7` | How long an unfulfilled key-distribution request lingers before cleanup. |
-| `pchatpendingfulfilledmaxhours` | `24` | How long a fulfilled key-distribution request stays around (so late joiners can pick it up). |
-| `pchatperuserpending` | `5` | Hard limit on simultaneous outstanding key requests per user. |
-| `pchatperchannelpendingsoftcap` | `100` | Soft cap on outstanding key requests per channel; prevents request floods. |
-
-A small set of token-bucket rate limits is also applied on the
-server (`msg`: 30 / 60s, `fetch`: 10 / 60s, `key_announce`: 5 / 60s,
-`key_exchange`: 20 / 60s). These are not currently configurable.
-
-## Channel modes
-
-Each channel has one persistence mode, chosen from the channel
-editor in the admin UI. New channels default to **None** and store
-nothing until an admin picks a mode.
-
-
-
-
-| | **None** | **Full Archive** | **Signal V1** |
-|---|---|---|---|
-| **History for newcomers** | - | Yes - full log up to the retention limit. | No - brand-new joiners get no backlog. |
-| **Missed messages on reconnect** | - | All missed messages are delivered by the server on reconnect - nothing is lost. | Automatically pushed on reconnect for existing members. |
-| **Server holds messages** | - | Yes, as encrypted ciphertext the operator cannot read. | Only a short-lived offline queue; no permanent archive. |
-| **If the server is seized** | - | Encrypted ciphertext is exposed (unreadable without the channel key). | Only the transient queue exists - no full history to hand over. |
-| **Works without Fancy Mumble** | Yes (live chat only) | Read-only if senders enable dual-path (see below) - no history, cannot send encrypted. | Read-only if senders enable dual-path (see below) - voice only otherwise, cannot send encrypted. |
-| **Setup** | Nothing to do. | Set the mode, optionally add key custodians. | Requires the `signal-bridge` library on every client. |
-| **Best for** | Ephemeral voice rooms. | Community and support channels where history is useful. | Small private groups where "no trace on the server" matters most. |
-
-
-**Full Archive** gives users the familiar "scroll up to catch up" experience with messages encrypted at rest. However, it is currently **experimental and unaudited** - treat it as transport encryption rather than true E2EE. Switch a channel to **Signal V1** for any channel where verified end-to-end encryption matters.
-
-
-
-
-
-| Mode (UI label) | Wire value | Server stores ciphertext? | Backfill on join | Key distribution |
-|---|---|:-:|---|---|
-| **None (standard volatile chat)** | `none` | - | - | - |
-| **Full Archive (all messages)** | `fancy_v1_full_archive` | yes | full history (subject to retention / message-count cap) | HMAC challenge against an online key custodian; relay cap scales with online member count |
-| **Signal V1 (E2EE via Signal Protocol)** | `signal_v1` | no - offline queue only | none for new joiners; existing members receive queued messages on reconnect | [Signal Sender Keys](https://signal.org/docs/specifications/sesame/) via `libsignal-protocol`; auto-verifies on `PchatKeyHolderReport`; relay cap 3 |
-
-
-The protobuf enum also defines `fancy_v1_post_join` and
-`server_managed` for backwards compatibility. The client UI does
-**not** expose them; treat them as deprecated.
-
-
-Things that often surprise people:
-
-- **"None" is the channel default.** The server-wide `pchatenabled` flag has no effect on a channel until an admin sets its mode.
-- **Neither persistent mode is readable by the server.** Full Archive stores opaque encrypted envelopes; Signal V1 never writes a permanent archive at all.
-- **Signal V1 has an offline queue, not a searchable archive.** When a message is sent the server queues it for every known offline key holder in `PChatOfflineQueueTable`. On reconnect, the server drains the queue and bundles the Sender Key Distributions (SKDMs) needed to decrypt each message - no online member required. A first-time joiner receives nothing; there is no backlog to send.
-
-
-
-
-## Encryption schemes
-
-The two persistent modes differ in *who* holds keys and *where*
-messages live.
-
-### Full Archive
-
-
-Full Archive (`fancy_v1_full_archive`) is currently **not recommended for production use**. The custom encryption scheme it uses has not undergone a formal security audit, and the key-distribution flow has known edge cases that can expose the channel key to a malicious server operator.
-
-Until a full audit is completed, treat Full Archive as **transport encryption only** - not end-to-end encryption. The server operator could, in principle, intercept keys and read message content. Use **Signal V1** for any channel where true E2EE guarantees matter.
-
-
-- The server keeps an opaque encrypted **envelope** per message,
- plus a small amount of metadata (`message_id`, `channel_id`,
- `sender_hash`, timestamps, optional `replaces_id`).
-- Symmetric channel keys are held by **key custodians** - a list of
- TLS certificate hashes attached to the channel
- (`pchat_key_custodians`). When a new member joins, they emit a
- `PchatKeyRequest`; the server relays it to online custodians up
- to a relay cap that scales with the number of online members. A
- custodian answers with a `PchatKeyExchange`, preceded by a
- `PchatKeyChallenge` (HMAC challenge so the custodian knows it is
- talking to a legitimate session).
-- If no custodian is online, the request is queued
- (`pchatpendingkeyrequestmaxdays`) and replayed when one connects.
-- Trust model: the server is honest-but-curious. It cannot read
- messages, but a malicious operator could replace the binary and
- intercept future key-exchange traffic. This is the same trust
- model as virtually every "encrypted at rest" group chat.
-
-### Signal V1
-
-- Uses the **Signal protocol's Sender Keys** for group messaging,
- via [signalapp/libsignal](https://github.com/signalapp/libsignal).
-- Because libsignal is **AGPL-3.0**, Fancy Mumble ships it as a
- separate shared library - `signal-bridge` - built from
- [`crates/signal-bridge`](https://github.com/Fancy-Mumble/FancyMumble/tree/master/crates/signal-bridge)
- as a `cdylib` and loaded at runtime via `libloading`. The MIT-
- licensed Fancy Mumble client links to it dynamically; the AGPL
- obligations stay scoped to that one `.dll` / `.so` / `.dylib`.
-- The server **does not permanently store** Signal V1 messages as a searchable archive. However, it **does** queue encrypted envelopes for known offline members (existing key holders) in a transient offline queue. When a known member reconnects, the server drains that queue and bundles the Sender Key Distributions (SKDMs) for each sender whose messages appear in it, so the client can decrypt them without needing any other member online. Fetch requests (history) against a Signal V1 channel still return empty - the offline queue is a push, not a pull.
-- A client without `signal-bridge` available cannot participate in a
- Signal V1 channel at all (it can still join the channel for
- voice).
-
-
-"Encryption uses the Signal protocol" is only true for channels in
-**Signal V1** mode. **Full Archive** uses a different, custom
-scheme that is *not* libsignal.
-
-
-## Per-channel settings
-
-In addition to picking a mode, an admin can set:
-
-| UI field | Wire field | Default source | Meaning |
-|---|---|---|---|
-| **Protocol** | `pchat_protocol` | `none` | Which mode applies to this channel. |
-| **Max history** | `pchat_max_history` | `pchatdefaultmaxhistory` | Max messages kept (Full Archive only). |
-| **Retention days** | `pchat_retention_days` | `pchatdefaultretentiondays` | Max age in days; `0` = forever (Full Archive only). |
-| **Key custodians** | `pchat_key_custodians` | empty | TLS cert hashes of users that hold and distribute the channel key. |
-
-To set them:
-
-
-1. Open the channel editor (right-click a channel → **Edit**).
-2. Open the **Protocol** dropdown and choose:
- - **None (standard volatile chat)** to disable persistence on this channel.
- - **Full Archive (all messages)** for an open searchable log; the server stores encrypted ciphertext.
- - **Signal V1 (E2EE via Signal Protocol)** for group E2EE. Missed messages are queued and pushed on reconnect; new joiners see no history.
-3. Set **Max history** and **Retention days** (ignored for Signal V1).
-4. Add the cert hashes of the **Key custodians** that should answer
- join-time key requests (Full Archive only; for Signal V1 the
- existing online members serve the same role automatically).
-5. Save.
-
-
-
-
-
-## Storage
-
-For **Full Archive** channels, encrypted envelopes live in the same
-SQLite database as the rest of the server state, at
-`/data/mumble-server.sqlite` inside the container. The schema
-includes separate tables for
-messages, user keys, member-join records, key-holder lists,
-pending-key-requests, the offline delivery queue, reactions, and
-pins.
-
-For high-traffic servers point the server at PostgreSQL by setting
-`MUMBLE_CONFIG_DBDRIVER=QPSQL` and friends.
-
-A rough sizing guide:
-
-- Each encrypted text envelope is roughly 200–400 bytes on disk
- including indexes and metadata.
-- 10 000 messages per day on a busy server is about 3 MB per day, so
- about 1 GB per year before reactions / images.
-- Inline images are base64-encoded inside the envelope. They will
- dominate disk usage if allowed; size `pchatmaxpayloadsize`
- accordingly.
-
-Plan disk space, and back the volume up regularly. See
-[Upgrade & backup](/server/upgrade/).
-
-Signal V1 channels store **nothing at rest**, so they contribute
-zero to ongoing disk usage.
-
-## Disabling persistence on a single channel
-
-To turn persistence off for a sensitive channel without touching the
-server-wide config, set the channel's **Protocol** back to **None
-(standard volatile chat)**. Existing stored ciphertext is not
-deleted by toggling - the server simply stops accepting new
-messages and stops serving fetches for that channel. To clear
-stored history, use the admin "Purge channel" action.
-
-## Disabling persistence server-wide
-
-```yaml
-environment:
- MUMBLE_CONFIG_PCHATENABLED: false
-```
-
-This makes the server reject every `Pchat*` message. Existing rows
-are not deleted, so you can re-enable later without data loss.
-
-## What clients see
-
-| Client | History on a Full Archive channel | Live chat in a Signal V1 channel |
-|---|:-:|:-:|
-| Fancy Mumble (desktop / Android, with `signal-bridge`) | yes | yes |
-| Fancy Mumble built without `signal-bridge` | yes | no - channel is voice-only for them |
-| Vanilla Mumble | no | no |
-| Other forks | depends on whether they implement the `Pchat*` proto messages | almost certainly no |
-
-Vanilla Mumble users still see live `TextMessage`s in real time on
-*any* channel - `Pchat*` is an additive layer on top of the existing
-text-message wire protocol.
-
-### Dual-path sending
-
-Users can enable **dual-path sending** in **Settings → Privacy →
-Enable dual-path sending**. When on, every message sent in an
-encrypted channel is also sent as a plain `TextMessage` whose body is
-replaced with `[Encrypted message]`. Legacy clients (vanilla Mumble,
-older forks) then show that placeholder instead of seeing nothing at
-all.
-
-Dual-path is **off by default**. Users who want strict ciphertext
-isolation - so that no unencrypted traffic for the channel ever
-traverses the normal message path - should leave it disabled.
-
-## Common pitfalls
-
-- **"My channel doesn't keep history."** Check the channel's
- **Protocol**, not just `pchatenabled` on the server. A new
- channel defaults to **None**.
-- **"Signal V1 shows no history when I reconnect."** For **existing members** the server queues missed messages and delivers them on reconnect automatically - if you see nothing, check that your `signal-bridge` library loaded correctly. If you are a **brand-new joiner**, that is expected: there is no backlog for first-time joins.
-- **"Key request just hangs."** For **Full Archive**: no key custodian is online; the request stays queued until one connects (up to `pchatpendingkeyrequestmaxdays`). For **Signal V1 - new joiner**: no online channel member is available to distribute the Sender Key; the request is similarly queued. Existing reconnecting Signal V1 members do **not** need a key request - the server bundles the necessary sender keys with the offline message queue automatically.
-- **"History grows forever."** `pchatdefaultretentiondays` (or the
- channel's `pchat_retention_days`) is `0`. Set a value, then
- cleanup runs daily.
-- **"Cannot delete a message."** The channel's delete ACL does not
- include your role. See [Roles & permissions](/admin/roles/).
-- **"Plugin keys aren't picked up via env vars."** They are - the
- Fancy Mumble Docker entrypoint accepts every key in the upstream
- `mumble-server.ini`, including `pchat*`. See
- [Configuration reference](/server/config/).
-
-## Next step
-
-Continue with [Push notifications](/server/features/push/).
+
diff --git a/src/content/docs/server/features/push.mdx b/src/content/docs/server/features/push.mdx
index b27099f..ed048d9 100644
--- a/src/content/docs/server/features/push.mdx
+++ b/src/content/docs/server/features/push.mdx
@@ -1,270 +1,12 @@
---
title: Push notifications
-description: Configure push notifications - Subscribe Push ACL for live desktop and mobile delivery, plus Firebase Cloud Messaging for offline Android notifications.
-sidebar:
- order: 2
+description: Platform notifications delivered by the Starling push service.
---
-import { Steps, Aside, Tabs, TabItem, Card, CardGrid } from '@astrojs/starlight/components';
+Starling's **push** service delivers notifications to registered client endpoints. Notification settings in the client determine what the user wants to receive; server provider credentials determine whether delivery can succeed.
-:::caution[Legacy C++ server instructions]
-This page documents the older mumble-server fork. Its INI keys, plugin paths, and ports do not configure Starling. For a new deployment, start with [Starling feature availability](/server/features/) and the [Starling configuration guide](/server/config/).
-:::
+Configure the provider supported by your Starling release, keep its credentials outside public source control, and verify the push service's startup logs. Use [Starling configuration reference](https://github.com/Fancy-Mumble/starling/blob/main/examples/reference.toml) for the current settings instead of copying configuration from another server implementation.
-Fancy Mumble delivers notifications through two separate paths:
+Test with a registered identity on the target platform. Confirm that the client has operating-system notification permission, then trigger an event while the app is in the background. Foreground desktop sounds and remote push delivery are separate paths.
-- **Live delivery (desktop and Android app while connected)** - The
- server routes `TextMessage`s from channels where the connecting client
- has the `SubscribePush` permission (`0x2000`). The desktop app fires
- a native OS notification for messages that arrive while the window is
- not focused or the channel is not the active one.
-- **Offline push (Android only)** - When the Android app is closed or
- backgrounded, **Firebase Cloud Messaging (FCM)** wakes it with a
- notification. There is no iOS client; Fancy Mumble is Android-only on
- mobile.
-
-The rest of this page covers the Subscribe Push ACL first, then the
-Firebase setup required for Android offline push.
-
-
-
-
-## Subscribe Push ACL
-
-The `SubscribePush` permission (`0x2000`) is a per-channel ACL flag that
-gates live notification delivery for every connected client - desktop and
-mobile alike.
-
-**How it works:** When a client connects, the server computes the set of
-channels where that client has `SubscribePush` and routes `TextMessage`s
-from those channels over the existing connection. The desktop app turns
-each routed message into a native OS notification when the window is
-unfocused or the message arrives in a background channel. Channels
-_without_ the permission are not routed at all - no notification fires,
-regardless of FCM setup.
-
-**Granting the permission:** Open the channel's ACL editor (right-click
-the channel → **Edit Permissions**), find or create the relevant role, and
-enable **Subscribe Push**. See [Roles & permissions](/admin/roles/) for
-the full ACL workflow.
-
-**Per-channel muting:** Users can suppress notifications for individual
-channels without revoking the permission by opening **Settings →
-Notifications** and toggling off specific channels. The muted list is
-synced to the server so those channels are excluded from live delivery
-even when the permission is granted.
-
----
-
-The rest of this page covers **Firebase Cloud Messaging (FCM)**, which is
-only needed for the Android offline-push path.
-
-
-The `MUMBLE_CONFIG_*` lines below belong in the `environment:` block
-of your `docker-compose.yml` (next to where you started the server).
-The `MUMBLE_FCM_CREDENTIALS_BASE64` env var goes in the same file or
-in the adjacent `.env` file. See
-[Configuration reference](/server/config/) for the file layout.
-
-
-## What you need
-
-- A free **Firebase project** at [console.firebase.google.com](https://console.firebase.google.com/).
-- A **service-account key** (JSON file) from that project.
-- Five minutes.
-
-## Step 1, create a Firebase project
-
-
-1. Open [console.firebase.google.com](https://console.firebase.google.com/).
-2. Click **Add project**, follow the wizard. Name it whatever you
- like.
-3. After creation, open **Project settings** (gear icon).
-4. Switch to the **Service accounts** tab.
-5. Click **Generate new private key**, then **Generate key**. A JSON
- file downloads.
-6. Note the **Project ID** at the top of the same page.
-
-
-
-Anyone with that file can send push notifications as your project.
-Do not commit it to a repository, do not paste it in chat.
-
-
-## Step 2, give the file to the server
-
-Pick the method that fits your deployment.
-
-
-
- ```bash
- docker secret create MUMBLE_FCM_CREDENTIALS \
- ./your-firebase-key.json
- ```
-
- Then reference it in compose:
-
- ```yaml
- services:
- mumble-server:
- secrets:
- - MUMBLE_FCM_CREDENTIALS
- environment:
- MUMBLE_CONFIG_PUSHENABLED: true
- MUMBLE_CONFIG_PUSHPROJECTID: your-firebase-project-id
-
- secrets:
- MUMBLE_FCM_CREDENTIALS:
- external: true
- ```
-
- The container reads `/run/secrets/MUMBLE_FCM_CREDENTIALS` at
- startup and the server picks it up automatically.
-
-
-
- Good for Kubernetes, CI deployments, or anywhere secret files
- are awkward.
-
- ```bash
- # Linux or macOS
- base64 -w 0 your-firebase-key.json
- ```
-
- Or in PowerShell:
-
- ```powershell
- [Convert]::ToBase64String([IO.File]::ReadAllBytes("your-firebase-key.json"))
- ```
-
- Pass the result as:
-
- ```yaml
- environment:
- MUMBLE_FCM_CREDENTIALS_BASE64: ""
- MUMBLE_CONFIG_PUSHENABLED: true
- MUMBLE_CONFIG_PUSHPROJECTID: your-firebase-project-id
- ```
-
- The [setup wizard](/server/wizard/) can do the encoding for you.
-
-
-
- ```yaml
- volumes:
- - ./fcm-credentials.json:/data/fcm-credentials.json:ro
- environment:
- MUMBLE_CONFIG_PUSHENABLED: true
- MUMBLE_CONFIG_PUSHPROJECTID: your-firebase-project-id
- MUMBLE_CONFIG_PUSHCREDENTIALSPATH: /data/fcm-credentials.json
- ```
-
- Do not use this in production. The file ends up readable on the
- host and can leak into image builds.
-
-
-
-## Step 3, pick what triggers a notification
-
-```yaml
-environment:
- MUMBLE_CONFIG_PUSHNOTIFYTEXTMESSAGE: true
- MUMBLE_CONFIG_PUSHNOTIFYREACTION: false
- MUMBLE_CONFIG_PUSHNOTIFYUSERJOIN: false
- MUMBLE_CONFIG_PUSHTOPICPREFIX: mumble
-```
-
-| Key | Default | Description |
-|-----|---------|-------------|
-| `pushenabled` | `false` | Master toggle. |
-| `pushprojectid` | `""` | Your Firebase project ID. |
-| `pushcredentialspath` | `""` | Where to read the JSON key. Set automatically by Docker secret. |
-| `pushtopicprefix` | `mumble` | FCM topic prefix used for grouping. |
-| `pushnotifytextmessage` | `true` | Send push on a new chat message. |
-| `pushnotifyreaction` | `false` | Send push when someone reacts. |
-| `pushnotifyuserjoin` | `false` | Send push when someone joins a channel you watch. |
-
-## Step 4, restart the server
-
-```bash
-docker compose restart mumble-server
-```
-
-Open the server logs and look for a line like:
-
-```text
-push: registered with project your-firebase-project-id
-```
-
-If you see that, you are live.
-
-## Step 5, ask your users to opt in
-
-**Android (offline FCM push)**
-
-
-1. Install the Fancy Mumble app on Android.
-2. Connect to your server at least once with the app open. The app
- registers an FCM device token at this point.
-3. Open **Settings → Notifications → Mobile push** and toggle which
- events to receive.
-
-
-
-There is no iOS client. Fancy Mumble is Android-only on mobile.
-
-
-**Desktop (live notifications)**
-
-No Firebase setup is needed for desktop notifications. The desktop app
-requests OS notification permission on first launch and fires native
-notifications automatically for channels where `SubscribePush` is
-granted. Users can manage per-channel muting under **Settings →
-Notifications**.
-
-## Cost
-
-FCM is **free** for the volumes a typical Mumble server generates.
-Google's "spark plan" allows unlimited messages.
-
-## Privacy
-
-The push payload contains:
-
-- The **channel name** the event happened in.
-- The **sender's username**.
-- A short **summary** of the event ("said: hello").
-- The **server's project ID**.
-
-It does **not** contain:
-
-- The plaintext message body of an encrypted persistent-chat
- message. Only a generic "new message" hint is sent.
-- Any token of identifying data about other users.
-
-If you want to disable the previews entirely, set
-`MUMBLE_CONFIG_PUSHCONTENTPREVIEW=false` (default `true`).
-
-## Disabling push
-
-To turn it off, set `pushenabled=false` and restart. The library is
-still in memory but does nothing.
-
-## Pitfalls
-
-- **No notifications arrive**: confirm `pushprojectid` matches the
- one in the Firebase console.
-- **Notifications arrive in delays of minutes**: the user's phone has
- battery saver enabled. Ask them to set the app to **Unrestricted**.
-- **Logs say "credentials missing"**: check that the secret is mounted
- at `/run/secrets/MUMBLE_FCM_CREDENTIALS`, or that
- `MUMBLE_FCM_CREDENTIALS_BASE64` is non-empty.
-
-## iOS
-
-Apple Push Notification Service is **not yet supported**. iOS users
-will only see notifications when the app is in the foreground.
-
-## Next step
-
-Continue with [Screen sharing relay](/server/features/webrtc-sfu/).
+See [Notifications](/users/notifications/) for user controls and [Starling service guide](https://github.com/Fancy-Mumble/starling/blob/main/docs/SERVICES.md) for the service's dependencies.
diff --git a/src/content/docs/server/features/reactions.mdx b/src/content/docs/server/features/reactions.mdx
index 5b25128..2a3ba40 100644
--- a/src/content/docs/server/features/reactions.mdx
+++ b/src/content/docs/server/features/reactions.mdx
@@ -12,7 +12,7 @@ Starling handles reactions and polls through its **social service**. The same se
The service is included in the default Starling stack. For a custom deployment, check that `social` is running and healthy. See [feature availability](/server/features/) and the [Starling service inventory](https://github.com/Fancy-Mumble/starling/blob/main/docs/SERVICES.md).
-
+
## Reactions
diff --git a/src/content/docs/server/features/watch-together.mdx b/src/content/docs/server/features/watch-together.mdx
index ec9a269..5f88940 100644
--- a/src/content/docs/server/features/watch-together.mdx
+++ b/src/content/docs/server/features/watch-together.mdx
@@ -1,70 +1,16 @@
---
title: Watch Together
-description: Synchronized video playback for everyone in a channel.
-sidebar:
- order: 7
+description: Synchronized video playback in a channel.
---
-import { Aside } from '@astrojs/starlight/components';
+Watch Together shares playback state with members of a channel. Starling's **social** service relays the start, pause, play, and seek updates. Each client fetches the video from its source.
-:::caution[Legacy C++ server instructions]
-This page documents the older mumble-server fork. Its INI keys, plugin paths, and ports do not configure Starling. For a new deployment, start with [Starling feature availability](/server/features/) and the [Starling configuration guide](/server/config/).
-:::
+## Start a session
-Watch Together lets one user in a channel play a video that is
-synchronized for everyone else. Play, pause, and seek events
-propagate to the other viewers.
+Open the attachment menu in chat and choose **Watch Together**, then provide a supported video URL. The client supports YouTube and direct media URLs such as MP4 or WebM. All viewers need access to the source and a codec their client can play.
+## Server requirements
-
+Keep the social service healthy and permit members to enter the channel. The server does not upload or proxy the video. Source authentication, region restrictions, and blocked embeds can make playback differ between viewers.
-## Supported sources
-
-Two source kinds are supported by the client today:
-
-- **YouTube**, played inside an embedded IFrame.
-- **Direct media URL** (a `.mp4` or `.webm` URL the client can fetch
- directly).
-
-Other providers (Vimeo, Twitch, and so on) are not implemented.
-
-## How it works
-
-The session is fully client-side. The server only relays plugin-data
-messages between participants:
-
-- The starter sends a "start" message with the source URL and kind.
-- Each viewer's app fetches the video from the source directly (the
- server does **not** proxy or stream it).
-- Play, pause, and seek actions are broadcast as small plugin-data
- messages.
-
-There are no server-side `MUMBLE_CONFIG_*` or `plugin.*` keys for
-Watch Together. Disabling it on a server requires building a custom
-server image.
-
-## Privacy
-
-The video is **not** streamed through the server. Each client
-fetches the content directly from YouTube or the URL host. The same
-caveats apply as for normal browser usage of those sites: the host
-sees each viewer's IP.
-
-## YouTube playback opt-in
-
-Some clients require the user to opt in to external embeds before
-the YouTube IFrame loads. A starter who picks a YouTube URL will
-see an explanation if their own client has not enabled the opt-in.
-
-## Pitfalls
-
-- **Out of sync after a long pause**: the next play / pause / seek
- event re-syncs everyone.
-- **Video is geo-blocked for some viewers**: the server cannot help.
- The viewer needs a working route to the provider.
-- **Direct URL refuses to play**: the file must serve `Accept-Ranges:
- bytes` and a video MIME type.
-
-## Next step
-
-Continue with [Whiteboard](/server/features/whiteboard/).
+See [Starling service guide](https://github.com/Fancy-Mumble/starling/blob/main/docs/SERVICES.md) for the service's role and [feature availability](/server/features/) for related setup.
diff --git a/src/content/docs/server/features/webrtc-sfu.mdx b/src/content/docs/server/features/webrtc-sfu.mdx
index 831a292..b8eacc0 100644
--- a/src/content/docs/server/features/webrtc-sfu.mdx
+++ b/src/content/docs/server/features/webrtc-sfu.mdx
@@ -1,344 +1,18 @@
---
-title: Screen sharing relay
-description: One upload, many downloads. Make screen sharing smooth for everyone in the channel.
-sidebar:
- order: 3
+title: Screen-share relay
+description: Deploy Starling screen sharing with a reachable UDP media address.
---
-import { Steps, Aside, Card, CardGrid, Tabs, TabItem } from '@astrojs/starlight/components';
-import PlantUML from '../../../../components/PlantUML.astro';
+Starling's **screenshare** service handles screen-share signalling and the server relay. Media uses a UDP listener separate from the Mumble voice port.
-:::caution[Legacy C++ server instructions]
-This page documents the older mumble-server fork. Its INI keys, plugin paths, and ports do not configure Starling. For a new deployment, start with [Starling feature availability](/server/features/) and the [Starling configuration guide](/server/config/).
-:::
+## Publish the media address
-The screen-share relay (technically a Selective Forwarding Unit, or
-SFU) is a small service inside the server that takes **one screen-
-share upload** from a broadcaster and **fans it out** to every viewer
-without the broadcaster doing extra work.
+Configure the screenshare service with a public media address reachable by clients, and open the matching UDP port in the firewall and container deployment. An HTTP reverse proxy alone cannot carry this UDP media path.
-Without the relay, screen sharing still works on a local network, but
-falls back to peer-to-peer, which is unreliable behind strict NAT or
-across the internet.
+The `media_port` option selects the media listener. A value of `0` lets the operating system choose a port, so use an explicit port when publishing a fixed firewall rule. Follow the [current example configuration](https://github.com/Fancy-Mumble/starling/blob/main/starling.example.toml) and [Starling configuration reference](https://github.com/Fancy-Mumble/starling/blob/main/examples/reference.toml) for the address fields supported by your release.
+## Verify outside the LAN
- SFU : 1× upload
-SFU --> V1 : fan-out
-SFU --> V2 : fan-out
-SFU --> VN : fan-out
-
-note bottom of SFU
- Broadcaster sends once.
- Server forwards to every viewer.
-end note
-@enduml
-`} />
-
-## When you want it
-
-- You have **more than two or three viewers** for a typical share.
-- Your users are spread across the internet.
-- Some users sit behind corporate NAT or carrier-grade NAT.
-
-If you only ever do 1-on-1 calls on the same network, you may not
-need it.
-
-
-The `ports:` and `environment:` lines below belong in your
-`docker-compose.yml` (the same file you started the server with).
-See [Configuration reference](/server/config/) for the file layout.
-
-
-## Turn it on
-
-```yaml
-ports:
- - "10000:10000/udp"
-environment:
- MUMBLE_CONFIG_WEBRTCSFUENABLED: true
- MUMBLE_CONFIG_WEBRTCSFUPORT: 10000
- MUMBLE_CONFIG_WEBRTCSFUPUBLICIP: "203.0.113.5"
-```
-
-| Key | Default | Description |
-|-----|---------|-------------|
-| `webrtcsfuenabled` | `false` | Master toggle. |
-| `webrtcsfuport` | `10000` | UDP port the relay listens on. |
-| `webrtcsfupublicip` | `127.0.0.1` | Address that **viewers** use to reach the relay. **Must be reachable**. |
-| `webrtcsfumodulepath` | (built-in) | Override if you want a custom relay binary. |
-
-
-Set `webrtcsfupublicip` to an address that your users can actually
-reach.
-
-- Public server: your server's public IP.
-- Home server with port forwarding: your public IP (not your LAN IP).
-- Test server on the same machine as the client: `127.0.0.1`.
-- Behind a load balancer: the load balancer's public IP.
-
-Do **not** set it to `0.0.0.0`, that is not a valid candidate.
-
-
-## Networking checklist
-
-
-1. UDP port **10000** is open in the firewall (or whatever you set
- `webrtcsfuport` to).
-2. UDP port **10000** is forwarded from your router to the server
- (home setups).
-3. The public IP your server **advertises** matches the one the
- internet can reach. Check with
- `curl -s https://ifconfig.me` from the server.
-
-
-## Verify it is working
-
-After a restart, the logs should include:
-
-```text
-webrtc-sfu: listening on 0.0.0.0:10000/udp (public 203.0.113.5)
-```
-
-Then ask two clients to test:
-
-
-1. Client A starts a screen share.
-2. Client B should see a thumbnail in the **stream grid**.
-3. If B sees a black thumbnail, the data path is broken. Check the
- public IP and the firewall first.
-
-
-
-The client shows which mode is active through two visual cues:
-
-**Screen-share button tooltip** (hover before starting a share):
-- **"Share screen (server-relayed)"** - the SFU is reachable and will be used.
-- **"Share screen (no SFU – peer-to-peer)"** - no SFU is configured or reachable; the share still works on a local network but will upload once per viewer.
-
-**Live indicators while a share is active** - colour tells you the transport at a glance:
-- **Red** LIVE badge (sidebar) and red broadcast banner = server-relayed via the SFU.
-- **Amber** LIVE badge and amber broadcast banner with a **P2P** pill = peer-to-peer, no SFU involved.
-
-If your server has the SFU enabled but users see amber indicators, double-check
-`webrtcsfupublicip` and the firewall.
-
-
-## How the pieces talk to each other
-
- SrvBox : TCP 64738 · control / signaling
-Bc --> SFU : UDP 10000 · one upload
-SFU --> VA : UDP
-SFU --> VB : UDP
-SFU --> VC : UDP
-
-note bottom
- Broadcaster upload is constant regardless of viewer count.
- The SFU handles every viewer independently.
-end note
-@enduml
-`} />
-
-- The broadcaster's app uploads one stream to the relay over **UDP**.
-- The relay fans the stream out to every viewer independently.
-- All the **signaling** (ICE negotiation, offer/answer) travels over the
- existing **TCP** voice/control port. The relay only carries media.
-- The broadcaster's upload bandwidth stays **constant** regardless of
- viewer count - that is the whole point.
-
-### Optional: behind a reverse proxy (nginx / Traefik / Caddy)
-
-A reverse proxy in front of the server is a common setup for TLS
-termination or port consolidation. Two ports are involved and they
-behave differently:
-
-| Port | Protocol | Proxy support |
-|------|----------|---------------|
-| 64738 (voice/control) | TCP | Standard TCP/HTTP proxy |
-| 10000 (SFU media) | **UDP** | Needs explicit UDP forwarding |
-
-The TCP side (signaling) passes through any standard TCP proxy with no
-special config. The UDP side requires the proxy to support raw UDP
-forwarding - not every proxy does by default.
-
-
-WebRTC media uses raw UDP datagrams, not HTTP. An HTTP reverse proxy
-(nginx in `http {}` mode, Traefik HTTP routers, Caddy `reverse_proxy`)
-will not forward it. You need the stream/UDP layer of the proxy.
-
-
-
-
- nginx's `stream {}` block handles raw TCP and UDP. Add it alongside
- your `http {}` block in `nginx.conf`:
-
- ```nginx
- stream {
- server {
- listen 10000 udp;
- # Replace with the address of the host running mumble-server.
- proxy_pass 127.0.0.1:10000;
- proxy_timeout 10s;
- proxy_responses 1;
- }
- }
- ```
-
- Tell the SFU to advertise the **proxy's** public IP, not the
- server's internal address:
-
- ```yaml
- environment:
- MUMBLE_CONFIG_WEBRTCSFUPUBLICIP: "203.0.113.5" # proxy public IP
- ```
-
- The `http {}` side (TLS termination, WebSocket for the control port)
- uses a standard `proxy_pass` directive and is not shown here.
-
-
-
- Traefik supports UDP entry points natively. Add a UDP entry point in
- `traefik.yml`:
-
- ```yaml
- entryPoints:
- sfu-udp:
- address: ":10000/udp"
- ```
-
- Then label the `mumble-server` container so Traefik routes it:
-
- ```yaml
- labels:
- - "traefik.udp.routers.sfu.entrypoints=sfu-udp"
- - "traefik.udp.services.sfu.loadbalancer.server.port=10000"
- ```
-
- Set the advertised IP to the Traefik host's public address:
-
- ```yaml
- environment:
- MUMBLE_CONFIG_WEBRTCSFUPUBLICIP: "203.0.113.5"
- ```
-
-
-
- The standard Caddy binary does not support raw UDP proxying. The
- community `layer4` plugin adds it:
-
- ```nginx
- {
- layer4 {
- :10000 {
- @sfu udp
- route @sfu {
- proxy 127.0.0.1:10000
- }
- }
- }
- }
- ```
-
- You need a custom Caddy build that includes the `layer4` module.
- For most setups nginx or Traefik are simpler choices for UDP.
-
-
-
-
-If the proxy and the server share the same public IP (e.g. both run on
-the same host), skip the UDP proxy entirely. Map the SFU port directly
-from the host and set `webrtcsfupublicip` to that host's public IP.
-The proxy handles TLS for the control port; UDP goes straight to the
-firewall rule.
-
-
-## Without the relay
-
-If `webrtcsfuenabled=false` (or the library is missing), the app
-falls back to direct peer-to-peer for screen sharing:
-
-- Works fine on a local network.
-- Works for two clients with no NAT.
-- Often fails behind strict NAT.
-- Broadcaster's upload scales linearly with viewer count.
-
-## Capacity planning
-
-A modest virtual server can host a relay for a few dozen viewers. For
-heavier use:
-
-- Each 720p stream is about 1 to 2 Mbps. Multiply by viewer count.
-- A 1 vCPU virtual server can handle around 20 to 30 viewer streams.
-- For a busy server, run the relay on a separate node and point
- `webrtcsfupublicip` at that node.
-
-## TURN servers (advanced)
-
-If your users sit behind extremely strict firewalls (some corporate
-networks, mobile carriers), even the relay may not be reachable on
-UDP 10000. The next step is a **TURN server** that relays media over
-443/TCP. Fancy Mumble does not yet ship a built-in TURN, but you can
-point clients at any standard TURN by setting their STUN/TURN config.
-
-## Disabling
-
-```yaml
-environment:
- MUMBLE_CONFIG_WEBRTCSFUENABLED: false
-```
-
-Existing streams stop immediately on restart. Clients fall back to
-peer-to-peer.
-
-## Pitfalls
-
-- **Black thumbnails**: public IP is wrong or unreachable.
-- **Logs say "library not found"**: the SFU binary was not built into
- your image. Either rebuild with the relay enabled (see
- [Building from source](/server/build/)) or use the official image.
-- **One-way streams**: ICE could not pick a candidate. Check the
- client's WebRTC stats from Expert mode.
-
-## Next step
-
-Continue with [File server](/server/features/file-server/).
+The client also supports a peer-to-peer path where available. The share control indicates the active delivery mode. See [Screen sharing](/users/screen-sharing/) and [Screen-share troubleshooting](/troubleshooting/screen-share/).
diff --git a/src/content/docs/server/features/whiteboard.mdx b/src/content/docs/server/features/whiteboard.mdx
index 11e4c09..f025cc4 100644
--- a/src/content/docs/server/features/whiteboard.mdx
+++ b/src/content/docs/server/features/whiteboard.mdx
@@ -1,54 +1,10 @@
---
-title: Whiteboard
-description: Real-time collaborative drawing on top of any screen share.
-sidebar:
- order: 8
+title: Whiteboard and drawing
+description: Drawing events shared with a channel or stream.
---
-import { Aside } from '@astrojs/starlight/components';
+The client can draw over a shared stream. Starling's **social** service relays drawing updates to the relevant channel; screen media is handled separately by the screenshare service.
-:::caution[Legacy C++ server instructions]
-This page documents the older mumble-server fork. Its INI keys, plugin paths, and ports do not configure Starling. For a new deployment, start with [Starling feature availability](/server/features/) and the [Starling configuration guide](/server/config/).
-:::
+Open a stream's drawing controls, choose a tool and colour, then draw on the shared view. Other viewers need a compatible Fancy Mumble client and access to the channel. If video works but drawing does not, check the social service and the client's feature availability.
-The whiteboard lets anyone watching a screen share **draw on top of
-it**. Useful for "point at this", code reviews, or just decorating a
-movie night.
-
-Strokes are sent through the server in real time, so every viewer
-sees them as they are drawn.
-
-
-
- Screenshot placeholder: stream with annotations layered on top.
-
-
-## How it works
-
-Each stroke is a small batch of points (position, color, width) sent
-as a plugin-data message. The server forwards the message to every
-participant in the channel. There is no per-stroke persistence; when
-the stream ends, the strokes are cleared.
-
-The whiteboard is a **client-side feature**. There are no documented
-server `MUMBLE_CONFIG_*` or `plugin.*` keys for tuning it.
-
-## Compatibility
-
-| Client | Can draw? | Can see strokes? |
-|--------|:---------:|:----------------:|
-| Fancy Mumble desktop | yes | yes |
-| Fancy Mumble mobile | no (view-only on mobile) | yes |
-| Vanilla Mumble | no | no |
-
-## Pitfalls
-
-- **Cannot draw on mobile**: the mobile build is view-only for the
- whiteboard. Use the desktop app to draw.
-
-## Next step
-
-Continue with [Live Documents](/server/features/live-doc/) for real-time
-collaborative document editing, or skip to
-[Customize & disable features](/server/customize/) for the consolidated
-toggle reference.
+See [Screen sharing](/users/screen-sharing/) for the workflow and [Starling service guide](https://github.com/Fancy-Mumble/starling/blob/main/docs/SERVICES.md) for the server components.
diff --git a/src/content/docs/server/migrating-to-starling.mdx b/src/content/docs/server/migrating-to-starling.mdx
new file mode 100644
index 0000000..1400dc8
--- /dev/null
+++ b/src/content/docs/server/migrating-to-starling.mdx
@@ -0,0 +1,50 @@
+---
+title: Migrating to Starling
+description: Move an end-of-life Fancy Mumble server deployment to Starling.
+---
+
+The Fancy Mumble C++ server fork and its Docker image are end of life. Move existing deployments to [Starling](/server/docker/). Keep the original deployment and a verified backup until the new server has passed your login, permission, voice, and feature checks.
+
+## 1. Back up and inventory
+
+Stop the old server before taking a consistent copy of its database. Preserve its configuration, TLS certificate and private key, uploaded files, plugin data, and any secrets mounted separately. Record the virtual server IDs, channel tree, registered accounts, groups, ACLs, bans, and features your community uses.
+
+Starling owns separate service databases. The importer does not turn an old database into a Starling database in place.
+
+## 2. Convert the configuration
+
+Run the migration command from the Starling release you will deploy:
+
+~~~sh
+starling migrate-config /backup/mumble-server.ini > starling.toml
+~~~
+
+Review the generated overlay and every warning. Configure Starling's data directory, service endpoints, public file and media addresses, TLS paths, and target instance. The old `MUMBLE_CONFIG_*` environment variables and `plugin.*` INI configuration are not the new deployment's runtime configuration. Use the [TOML reference](https://github.com/Fancy-Mumble/starling/blob/main/examples/reference.toml).
+
+## 3. Preview the database import
+
+The importer reads a SQLite source and writes to the service databases selected by the target TOML. The usual single-server deployment uses Starling instance **1**; the old virtual server ID may be **0**. Confirm both IDs for your deployment.
+
+~~~sh
+starling migrate-db --from sqlite:/backup/mumble-server.sqlite --server-id 0 --instance 1 --config starling.toml --dry-run
+~~~
+
+Read the counts and reported unmapped values before writing. The import covers registered users, channels and links, ACLs and groups, bans, and mapped instance settings. Existing password hashes are verified in their imported form and upgraded after a successful password login.
+
+## 4. Import and verify
+
+~~~sh
+starling migrate-db --from sqlite:/backup/mumble-server.sqlite --server-id 0 --instance 1 --config starling.toml --verify
+~~~
+
+The importer is resumable and uses upserts. Verification compares source and destination entity counts. Use the [Starling storage and migration guide](https://github.com/Fancy-Mumble/starling/blob/main/docs/STORAGE.md#4-the-migration-tool) for `--table-prefix`, multi-instance imports, and exact coverage in your release.
+
+Persistent chat history and plugin-owned data are not carried by the core database importer. Preserve their original backups and plan their migration separately. Uploaded file bytes and provider credentials also need their own transfer; do not treat matching database counts as a complete feature migration.
+
+## 5. Test and switch clients
+
+Start Starling on a temporary address or port. Test registered certificate and password login, guest access, channel permissions, moderation, voice over UDP and TCP, file downloads, screen sharing, and every advertised plugin you use. Compare the channel tree and account counts with the inventory.
+
+Preserving the original TLS certificate avoids an unnecessary server fingerprint change. If you intentionally replace it, tell members the new fingerprint before switching the public address.
+
+Switch the public address after these checks. Keep the old deployment stopped and its backup intact until you have confirmed the new server and its backups. Continue with [Upgrade and backup](/server/upgrade/).
diff --git a/src/content/docs/server/network.mdx b/src/content/docs/server/network.mdx
index 8cf60a5..62be813 100644
--- a/src/content/docs/server/network.mdx
+++ b/src/content/docs/server/network.mdx
@@ -7,7 +7,7 @@ For the maintained Starling Compose deployment, expose **64738/TCP** for the Mum
The Compose file also publishes **8080/TCP** for HTTP file transfers. Its sample public_url points to localhost; replace that URL with an address your users can reach, normally HTTPS through a reverse proxy. File links cannot work for remote clients while they point at localhost.
-Screen-share media uses a separate UDP listener when the relay is configured. Set the screenshare service public_url to a reachable literal IP and port; open that UDP port in your firewall. The old C++ server's port 10000 is not a Starling default. The [example TOML](https://github.com/Fancy-Mumble/starling/blob/main/starling.example.toml) shows the relevant settings.
+Screen-share media uses a separate UDP listener when the relay is configured. Set the screenshare service public_url to a reachable literal IP and port; open that UDP port in your firewall. The [example TOML](https://github.com/Fancy-Mumble/starling/blob/main/starling.example.toml) shows the relevant settings.
The optional operator API is plain HTTP and should remain on a trusted network. The Compose admin profile binds its port 8081 to the host loopback interface. See the [operator API guide](https://github.com/Fancy-Mumble/starling/blob/main/docs/OPERATOR-API.md) before exposing it through a proxy.
diff --git a/src/content/docs/server/plugins/developing.mdx b/src/content/docs/server/plugins/developing.mdx
index 81b1e77..a8afd7b 100644
--- a/src/content/docs/server/plugins/developing.mdx
+++ b/src/content/docs/server/plugins/developing.mdx
@@ -1,431 +1,12 @@
---
-title: Developing a plugin
-description: Build, package, and ship your own Fancy Mumble server plugin as a Rust cdylib.
-sidebar:
- order: 3
+title: Develop a Starling plugin
+description: Use the current host interfaces and examples for a server plugin.
---
-import { Steps, Aside, Tabs, TabItem, Code } from '@astrojs/starlight/components';
+Build against the plugin API shipped with the Starling release you target. Starling's host exposes scoped configuration, membership, permissions, channel operations, and messaging to native libraries and WebAssembly components.
-:::caution[Legacy C++ server instructions]
-This page documents the older mumble-server fork. Its INI keys, plugin paths, and ports do not configure Starling. For a new deployment, start with [Starling feature availability](/server/features/) and the [Starling configuration guide](/server/config/).
-:::
+Start from the native and WASM examples in the [Starling repository](https://github.com/Fancy-Mumble/starling). Match their ABI and component interface versions. Keep plugin messages opaque to the host and validate every payload in the plugin that owns it.
-This guide walks through writing a Fancy Mumble server plugin from
-scratch. Plugins are Rust `cdylib` crates that depend on the
-[`mumble-plugin-api`] crate and are loaded at runtime by the plugin
-host. By the end of this page you will have a plugin that:
+Test loading, registry advertisement, connect and disconnect events, permission failures, restart behaviour, and enable/disable cycles. A native plugin executes inside the plugins service process; WebAssembly has a different capability boundary and must be tested separately.
-- Loads on server boot and logs a banner.
-- Reads its own configuration block from `mumble-server.ini`.
-- Advertises capabilities and live stats to clients through the
- registry.
-- Receives a `PluginMessage` from the client and replies with one.
-
-
-A ready-to-clone scaffold matching this guide lives at
-[Fancy-Mumble/fancy-plugin-example]. It already wires up the
-`cdylib` crate, the `mumble-plugin-api` dependency, and a minimal
-`MumblePlugin` impl, so you can skip straight to writing your own
-hooks.
-
-
-[`mumble-plugin-api`]: https://github.com/Fancy-Mumble/mumble-server/tree/main/3rdparty/mumble-plugin-host/api
-[Fancy-Mumble/fancy-plugin-example]: https://github.com/Fancy-Mumble/fancy-plugin-example
-
-## Prerequisites
-
-- **Rust 1.78+** (stable). The plugin host pins `abi_stable 0.11`,
- which compiles cleanly on any recent stable toolchain.
-- A copy of the [Mumble server repository] so you can pull in the
- `mumble-plugin-api` crate by path (until a release is published to
- crates.io).
-- A working server build to test against -
- [Docker quick start](/server/docker/) is the fastest path.
-
-[Mumble server repository]: https://github.com/Fancy-Mumble/mumble-server
-
-## Create the crate
-
-```bash
-cargo new --lib fancy-hello
-cd fancy-hello
-```
-
-Replace `Cargo.toml` with:
-
-```toml
-[package]
-name = "fancy-hello"
-version = "0.1.0"
-edition = "2021"
-license = "MIT"
-
-[lib]
-# cdylib - so the host can dlopen it.
-# rlib - so you can write integration tests in Rust.
-crate-type = ["cdylib", "rlib"]
-
-[dependencies]
-mumble-plugin-api = { path = "../mumble-server/3rdparty/mumble-plugin-host/api" }
-abi_stable = "0.11"
-serde = { version = "1", features = ["derive"] }
-serde_json = "1"
-tracing = "0.1"
-```
-
-
-Use a `path` dependency while iterating. Once you publish your plugin
-you can switch to a `git` or `version` reference on a tagged release
-of `mumble-plugin-api`.
-
-
-## Implement the plugin trait
-
-The whole plugin lives in `src/lib.rs`. Every method on
-`MumblePlugin` has a no-op default, so you only override the ones you
-care about.
-
-```rust
-use abi_stable::std_types::{RArc, ROk, RStr, RString};
-use mumble_plugin_api::{
- ClientInfo, DebugRow, MumblePlugin, PluginContext_TO, PluginError,
- PluginInfo, PluginMessageIn, PluginMessageOut, PluginResult, ServerId,
- SessionId, fancy_export_plugin,
-};
-use serde::{Deserialize, Serialize};
-use std::sync::{Arc, Mutex};
-
-const PLUGIN_NAME: &str = "fancy-hello";
-const PLUGIN_VERSION: &str = env!("CARGO_PKG_VERSION");
-
-#[derive(Default)]
-pub struct HelloPlugin {
- ctx: Mutex>>>,
- greeting: Mutex,
-}
-
-impl HelloPlugin {
- pub fn new() -> Self {
- Self::default()
- }
-}
-
-impl MumblePlugin for HelloPlugin {
- fn name(&self) -> RStr<'_> {
- RStr::from(PLUGIN_NAME)
- }
-
- fn version(&self) -> RStr<'_> {
- RStr::from(PLUGIN_VERSION)
- }
-
- fn info_json(&self) -> RString {
- let greeting = self
- .greeting
- .lock()
- .map(|g| g.clone())
- .unwrap_or_default();
- let info = PluginInfo {
- description: "Toy plugin that echoes a configurable greeting.".into(),
- author: Some("Me".into()),
- homepage: None,
- capabilities: vec!["demo".into()],
- debug_rows: vec![DebugRow {
- label: "greeting".into(),
- value: greeting,
- }],
- };
- match info.to_validated_json() {
- Ok(bytes) => RString::from(String::from_utf8_lossy(&bytes).into_owned()),
- Err(_) => RString::from("{}"),
- }
- }
-
- fn on_load(&self, ctx: PluginContext_TO>) -> PluginResult<()> {
- let greeting = ctx
- .get_config(RStr::from("greeting"))
- .into_option()
- .map(|s| s.into_string())
- .unwrap_or_else(|| "Hello, world!".to_owned());
- tracing::info!(%greeting, "fancy-hello: loaded");
- if let Ok(mut g) = self.greeting.lock() {
- *g = greeting;
- }
- if let Ok(mut slot) = self.ctx.lock() {
- *slot = Some(ctx);
- }
- ROk(())
- }
-
- fn on_plugin_message(&self, msg: PluginMessageIn) -> PluginResult<()> {
- if msg.payload_type.as_str() != "Ping" {
- return ROk(());
- }
- let Ok(ctx_guard) = self.ctx.lock() else { return ROk(()); };
- let Some(ctx) = ctx_guard.as_ref() else { return ROk(()); };
- let greeting = self
- .greeting
- .lock()
- .map(|g| g.clone())
- .unwrap_or_else(|_| "Hello!".into());
-
- let payload = serde_json::to_vec(&PongPayload {
- greeting,
- echoed_from: msg.sender_name.as_str().to_owned(),
- })
- .unwrap_or_default();
-
- let reply = PluginMessageOut {
- server_id: msg.server_id,
- plugin_name: RString::from(PLUGIN_NAME),
- payload_type: RString::from("Pong"),
- payload: payload.into(),
- target_sessions: vec![msg.sender_session].into(),
- channel_id: abi_stable::std_types::RNone,
- };
- if let abi_stable::std_types::RResult::RErr(e) =
- ctx.send_plugin_message(reply)
- {
- tracing::warn!(error = ?e, "fancy-hello: send_plugin_message failed");
- }
- ROk(())
- }
-}
-
-#[derive(Serialize, Deserialize)]
-struct PongPayload {
- greeting: String,
- echoed_from: String,
-}
-
-// Exports the cdylib entry point. Without this, the host cannot
-// instantiate the plugin.
-fancy_export_plugin!(HelloPlugin::new);
-```
-
-A few things to call out:
-
-- **`PLUGIN_NAME` is your wire identity.** Clients address you by this
- string. Keep it stable across versions; bumping it is a breaking
- change for every client that knows about your plugin.
-- **The host gates `on_load` on `plugin..enabled`.** If the
- operator has not set it to a truthy value
- (`true`/`1`/`yes`/`on`), your plugin's `on_load` is never called
- and the plugin is excluded from the registry. You never need to
- read `enabled` yourself.
-- **The host strips the `plugin..` prefix** before calling
- `get_config`, so you query for the *short* key (`"greeting"`,
- `"max_pings_per_minute"`, ...).
-- **`payload` is opaque bytes.** Use whatever serialisation you like.
- JSON keeps client/server symmetry easy; protobuf gives you typed
- schemas if you want them.
-- **Stash the `PluginContext`** somewhere you can read from
- `on_plugin_message`. The trait object is `Clone + Send + Sync`, so a
- `Mutex>` (or `OnceLock`) is fine.
-
-## Build it
-
-```bash
-cargo build --release
-```
-
-The output lands at `target/release/libfancy_hello.so` on Linux,
-`libfancy_hello.dylib` on macOS, or `fancy_hello.dll` on Windows.
-
-
-The plugin host is built into the same binary as `murmurd`. To ship a
-plugin for the official `fancy-mumble/mumble-server` image, build it
-inside the same Linux/glibc toolchain - typically Ubuntu 24.04. The
-easiest way to guarantee compatibility is to compile inside a
-`fancy-mumble/mumble-server` builder stage of your own Dockerfile.
-
-
-## Deploy it
-
-
-1. Drop the cdylib into a host folder, e.g. `./my-plugins/`.
-2. Mount it into the container at `/etc/mumble/plugins`:
-
- ```yaml
- volumes:
- - ./my-plugins:/etc/mumble/plugins:ro
- ```
-
-3. Add the plugin's config block to `mumble-server.ini`:
-
- ```ini
- plugin.fancy-hello.enabled=true
- plugin.fancy-hello.greeting=Welcome to my server!
- ```
-
-4. Restart the container. You should see:
-
- ```text
- INFO mumble_plugin_host::host: plugin loaded plugin=fancy-hello version=0.1.0 path=/etc/mumble/plugins/libfancy_hello.so
- INFO fancy_hello: fancy-hello: loaded greeting="Welcome to my server!"
- ```
-
-
-## Talk to the plugin from the client
-
-Once the server is running, any Fancy Mumble client at version
-`>= 0.4.0` learns about your plugin via the registry. To send it a
-ping from the client-side code (or from a console plugin), call the
-generic Tauri command:
-
-```ts
-import { invoke } from "@tauri-apps/api/core";
-
-const payload = new TextEncoder().encode(JSON.stringify({ when: Date.now() }));
-await invoke("send_plugin_message", {
- pluginName: "fancy-hello",
- payloadType: "Ping",
- payload: Array.from(payload),
- // Empty target list + null channel = "send to self/server only";
- // your plugin can echo back via PluginContext::send_plugin_message.
- targetSessions: [],
- channelId: null,
-});
-```
-
-Pong replies arrive as Tauri events with the channel
-`plugin-message`. Subscribe via:
-
-```ts
-import { listen } from "@tauri-apps/api/event";
-
-await listen<{
- plugin_name: string;
- payload_type: string;
- payload: number[];
- sender_session: number | null;
-}>("plugin-message", (e) => {
- if (e.payload.plugin_name !== "fancy-hello") return;
- if (e.payload.payload_type !== "Pong") return;
- const json = JSON.parse(new TextDecoder().decode(new Uint8Array(e.payload.payload)));
- console.log("hello plugin says:", json.greeting);
-});
-```
-
-## Lifecycle hooks
-
-All hooks on [`MumblePlugin`] are optional. The most useful ones:
-
-| Hook | When it fires | Typical use |
-|------|---------------|-------------|
-| `on_load(ctx)` | Once at boot, after the cdylib is opened. | Parse config, bind sockets, start tasks. |
-| `on_unload()` | Once at server shutdown. | Flush state, close sockets, await tasks. |
-| `on_client_connected(info)` | Right after a client authenticates. | Per-session bookkeeping. |
-| `on_client_disconnected(server, session)` | When a client disconnects. | Clean up per-session state. |
-| `on_plugin_message(msg)` | Inbound wire-200 envelope targeted at your `name()`. | Main RPC entry point. |
-| `on_plugin_data(server, sender, id, bytes)` | Inbound legacy `PluginDataTransmission`. | Backwards compat with pre-0.4.0 clients. |
-
-Hooks are **synchronous**. If you need async I/O, spin up a private
-`tokio::runtime::Runtime` in `on_load` and `block_on` inside the
-hook. The host catches panics at the FFI boundary, so a panicking
-runtime only affects your plugin.
-
-## Configuration parsing
-
-A typical pattern is to gather all your config keys into a struct
-during `on_load`:
-
-```rust
-struct HelloConfig {
- greeting: String,
- max_pings_per_minute: u32,
-}
-
-impl HelloConfig {
- fn from_context(ctx: &PluginContext_TO>) -> Result {
- let greeting = read_string(ctx, "greeting").unwrap_or_else(|| "Hello!".into());
- let max_pings_per_minute = read_u32(ctx, "max_pings_per_minute").unwrap_or(60);
- Ok(Self { greeting, max_pings_per_minute })
- }
-}
-```
-
-The helper functions wrap `ctx.get_config(RStr::from(key)).into_option()`
-and parse the result. The `LiveDocConfig` in
-`3rdparty/mumble-plugin-host/live-doc/src/config.rs` is a good
-reference implementation.
-
-## Advertising capabilities
-
-Whatever you return from `info_json()` ends up in the
-`PluginRegistry` envelope and is rendered verbatim in the developer
-Server Info panel. The recommended shape is the typed `PluginInfo`
-struct:
-
-```rust
-PluginInfo {
- description: "One-line summary".into(),
- author: Some("Your name".into()),
- homepage: Some("https://example.com/my-plugin".into()),
- capabilities: vec!["websocket".into(), "persistence".into()],
- debug_rows: vec![
- DebugRow { label: "port".into(), value: "9000".into() },
- DebugRow { label: "active_sessions".into(), value: count.to_string() },
- ],
-}
-.to_validated_json()
-```
-
-`to_validated_json` enforces a 64 KiB cap so a runaway plugin cannot
-flood the control channel. If you need to ship larger payloads, send
-them as a normal `PluginMessage` to interested sessions rather than
-piggy-backing on the registry.
-
-## Permissions
-
-If your plugin gates anything on Mumble ACLs, use
-`PluginContext::has_permission`:
-
-```rust
-use mumble_plugin_api::permissions::{WRITE, ENTER};
-
-if !ctx.has_permission(server_id, session, channel, WRITE | ENTER) {
- return ROk(()); // silently drop unauthorised requests
-}
-```
-
-The full flag table is in `mumble_plugin_api::permissions`. Combine
-flags with bitwise OR. The check uses the same ACL evaluator the
-server itself uses for client requests.
-
-## Testing
-
-Because the plugin is also an `rlib`, you can write normal integration
-tests against the trait without going through the FFI:
-
-```rust
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn ping_pong_round_trip() {
- let plugin = HelloPlugin::new();
- // Stub the context with a fake that records send_plugin_message
- // calls into a Vec, then drive on_plugin_message and assert.
- }
-}
-```
-
-For end-to-end coverage, point a real server at your built cdylib and
-exercise the wire path from a Fancy Mumble client.
-
-## Where to read source
-
-The plugins shipped with the official image are the best reference:
-
-- [Fancy-Mumble/fancy-plugin-example] - minimal standalone template
- you can fork as the starting point for a new plugin.
-- `3rdparty/mumble-plugin-host/file-server` - HTTP storage plugin.
-- `3rdparty/mumble-plugin-host/live-doc` - WebSocket + Yjs CRDT, the
- most feature-complete example. Walk through `on_load`,
- `on_plugin_message`, and `host_facade.rs` to see the patterns in
- context.
-- `3rdparty/mumble-plugin-host/api` - the trait definitions; every
- doc comment is the canonical contract.
+Read the [plugin host plan](https://github.com/Fancy-Mumble/starling/blob/main/docs/PLUGIN-HOST-PLAN.md) for implemented host calls and remaining gaps before depending on a capability.
diff --git a/src/content/docs/server/plugins/overview.mdx b/src/content/docs/server/plugins/overview.mdx
index 5ba2156..952b1c1 100644
--- a/src/content/docs/server/plugins/overview.mdx
+++ b/src/content/docs/server/plugins/overview.mdx
@@ -1,158 +1,19 @@
---
-title: Plugin system overview
-description: How the Fancy Mumble server's Rust plugin host loads cdylibs, advertises capabilities, and brokers messages with the client.
-sidebar:
- order: 1
+title: Starling plugin system
+description: How Starling loads plugins and advertises their capabilities.
---
-import { Aside, Card, CardGrid } from '@astrojs/starlight/components';
-import PlantUML from '../../../../components/PlantUML.astro';
+Starling runs plugins inside its **plugins** service. Native plugin libraries and WebAssembly components use the host's scoped capabilities for configuration, sessions, channels, permissions, and messaging.
-:::caution[Legacy C++ server instructions]
-This page documents the older mumble-server fork. Its INI keys, plugin paths, and ports do not configure Starling. For a new deployment, start with [Starling feature availability](/server/features/) and the [Starling configuration guide](/server/config/).
-:::
+The service reads its host options from `[services.plugins.options]`. Configure `plugins_dir` to the directory containing compatible plugin artifacts. With no directory configured, no plugins are loaded.
-Fancy Mumble extends the classic Mumble server with a **Rust plugin
-host**. Features like the persistence-backed file store, the
-collaborative live-doc editor, screen-share signalling glue, and the
-push-notification bridge are all built as `cdylib` plugins loaded at
-startup from a directory on disk.
+~~~toml
+[services.plugins.options]
+plugins_dir = "/var/lib/starling/plugins"
+~~~
-The host is intentionally *loosely coupled*: the server does not know
-about specific plugins. A plugin registers itself by being present in
-the plugin directory, advertises its capabilities to clients at
-connect time, and receives addressed messages through a single
-generic envelope.
+Per-plugin settings use the `plugin..` prefix inside that options table. Read the plugin's own documentation for its keys. After startup, check the service logs and advertised registry; a plugin file on disk does not prove that it loaded successfully.
-## Architecture
+The host supports listing, enabling, disabling, and uninstalling loaded plugins. Installation from remotely uploaded bytes and the operator REST plugin routes remain unfinished in the current host plan. Place artifacts through your deployment tooling and use only the administration operations supported by your release.
- Server : Wire 200\\nPluginMessage
-Server -> Host : C ABI
-Host --> PluginA : abi_stable trait
-Host --> PluginB : abi_stable trait
-Host --> PluginC : abi_stable trait
-Server -> App : Wire 201\\nPluginRegistry\\n(after ServerSync)
-@enduml
-`} />
-
-The server loads `libmumble_plugin_host.so` once at boot. That cdylib
-scans every configured plugin directory, `dlopen`s each entry, and
-exposes a single C ABI back to the C++ server. Every Mumble plugin is
-itself a cdylib that depends on the `mumble-plugin-api` crate.
-
-## Generic message envelope
-
-Plugins do not get their own protobuf message types. Instead, the
-client and server speak two generic envelopes:
-
-| Wire ID | Name | Direction | Purpose |
-|--------:|------|-----------|---------|
-| `200` | `PluginMessage` | Bidirectional | Addressed plugin payload. |
-| `201` | `PluginRegistry` | Server -> Client | Announces loaded plugins after `ServerSync`. |
-
-A `PluginMessage` carries:
-
-- `plugin_name` (string) - the stable identifier of the target plugin.
-- `payload_type` (string) - a plugin-defined inner discriminator
- (`"OpenRequest"`, `"Invite"`, `"Vote"`, ...).
-- `payload` (bytes) - opaque bytes; each plugin picks its own encoding
- (JSON, protobuf, MessagePack, ...).
-- `target_sessions` (repeated u32) - explicit recipient sessions.
-- `channel_id` (optional u32) - channel-scoped fan-out hint.
-- `sender_session` / `sender_name` - stamped by the server on inbound.
-
-The server routes inbound envelopes to exactly **one** plugin (the one
-whose `name()` matches `plugin_name`). On outbound, the server delivers
-to every session in `target_sessions`, or - if that list is empty - to
-every member of `channel_id`. Clients that report a Fancy version
-older than 0.4.0 are skipped.
-
-## Plugin registry
-
-Right after the server delivers `ServerSync` to a connecting client,
-it sends a `PluginRegistry` listing every loaded plugin:
-
-```json
-{
- "plugins": [
- {
- "plugin_name": "fancy-live-doc",
- "version": "0.1.0",
- "plugin_slot": 0,
- "info_json": "{...}"
- },
- {
- "plugin_name": "fancy-file-server",
- "version": "0.1.0",
- "plugin_slot": 1,
- "info_json": "{...}"
- }
- ]
-}
-```
-
-The `info_json` blob is whatever the plugin returned from
-`MumblePlugin::info_json()`. For Fancy-built plugins this is a typed
-`PluginInfo` struct serialised as JSON, hard-capped at 64 KiB. Clients
-use it to populate the developer Server Info panel and to decide
-whether a feature is available on this server.
-
-
-The registry only goes to clients with `fancy_version >= 0.4.0`. The
-plugin host itself runs unchanged on older client connections - they
-simply never learn about plugins and never send wire-200 messages.
-
-
-## ABI stability
-
-Plugins are loaded across an `abi_stable`-based FFI boundary
-([abi_stable on crates.io](https://crates.io/crates/abi_stable)).
-That means:
-
-- Plugins can be built with a **different `rustc` version** than the
- host and still load cleanly.
-- The host **refuses to load** any cdylib whose `abi_version` does not
- match `PLUGIN_ABI_VERSION` (currently `2`). When the trait surface
- changes, that constant is bumped and every plugin must be rebuilt.
-- Every trait method receives FFI-safe types (`RString`, `RSlice`,
- `RVec`, `ROption`, `RArc`) instead of their `std` counterparts.
-
-## Fault isolation
-
-Each plugin owns its **own private `tokio` runtime**. A plugin
-panicking inside an async task only takes down its own runtime; the
-host catches the panic at the FFI boundary, logs an error, and the
-rest of the server continues running. Other plugins are unaffected.
-
-## What's next
-
-
-
- Where the plugin directory lives, how to enable plugins,
- configuration key naming, and the built-in plugins shipped with
- every Fancy Mumble image. See [Using plugins](/server/plugins/using/).
-
-
- A complete walk-through of building a `cdylib` plugin from scratch
- in Rust, registering it with the host, and round-tripping a
- message with the client. See [Developing a plugin](/server/plugins/developing/).
-
-
+See the [plugin host implementation and plan](https://github.com/Fancy-Mumble/starling/blob/main/docs/PLUGIN-HOST-PLAN.md), [Starling configuration reference](https://github.com/Fancy-Mumble/starling/blob/main/examples/reference.toml), and [feature availability](/server/features/).
diff --git a/src/content/docs/server/plugins/using.mdx b/src/content/docs/server/plugins/using.mdx
index 0196889..af49ff4 100644
--- a/src/content/docs/server/plugins/using.mdx
+++ b/src/content/docs/server/plugins/using.mdx
@@ -1,200 +1,19 @@
---
-title: Using plugins
-description: How to install, configure, and disable plugins on a Fancy Mumble server.
-sidebar:
- order: 2
+title: Deploy Starling plugins
+description: Install compatible artifacts and verify their advertised registry.
---
-import { Steps, Aside, Tabs, TabItem } from '@astrojs/starlight/components';
+Starling runs plugins inside its **plugins** service. Native plugin libraries and WebAssembly components use the host's scoped capabilities for configuration, sessions, channels, permissions, and messaging.
-:::caution[Legacy C++ server instructions]
-This page documents the older mumble-server fork. Its INI keys, plugin paths, and ports do not configure Starling. For a new deployment, start with [Starling feature availability](/server/features/) and the [Starling configuration guide](/server/config/).
-:::
+The service reads its host options from `[services.plugins.options]`. Configure `plugins_dir` to the directory containing compatible plugin artifacts. With no directory configured, no plugins are loaded.
-This page covers operating plugins on a running server: where the host
-looks for them, how configuration keys are namespaced, how to verify
-that a plugin loaded correctly, and a tour of the plugins that ship
-with the official Docker image.
+~~~toml
+[services.plugins.options]
+plugins_dir = "/var/lib/starling/plugins"
+~~~
-## Plugin directories
+Per-plugin settings use the `plugin..` prefix inside that options table. Read the plugin's own documentation for its keys. After startup, check the service logs and advertised registry; a plugin file on disk does not prove that it loaded successfully.
-The host scans two directories at startup:
+The host supports listing, enabling, disabling, and uninstalling loaded plugins. Installation from remotely uploaded bytes and the operator REST plugin routes remain unfinished in the current host plan. Place artifacts through your deployment tooling and use only the administration operations supported by your release.
-| Path | Purpose |
-|------|---------|
-| `/usr/lib/mumble-server/plugins` | Plugins baked into the image. |
-| `/etc/mumble/plugins` | Operator-supplied plugins (mount your own). |
-
-Both are scanned on boot. Any file matching the platform's shared
-library extension (`*.so` on Linux, `*.dylib` on macOS, `*.dll` on
-Windows) is considered a candidate. Files that fail to load (wrong
-ABI version, missing symbols, panic in `on_load`, ...) are logged and
-skipped; the server continues to boot.
-
-To override the search path, set the `plugins_dir` server config key
-(comma-separated for multiple directories).
-
-## Configuration namespacing
-
-Every plugin gets its own configuration namespace under `plugin..*`
-in `mumble-server.ini`, where `` is the value returned by the
-plugin's `MumblePlugin::name()` method.
-
-```ini
-# Enable the live-doc plugin and bind its WebSocket on port 64740
-plugin.fancy-live-doc.enabled=true
-plugin.fancy-live-doc.port=64740
-plugin.fancy-live-doc.state_path=/data/fancy-live-doc
-
-# Enable the file server and pin its storage directory
-plugin.fancy-file-server.enabled=true
-plugin.fancy-file-server.storagePath=/data/file-server-storage
-plugin.fancy-file-server.port=64739
-```
-
-Inside the plugin, these are read via
-`PluginContext::get_config("port")` - the host strips the
-`plugin..` prefix automatically, so plugins never have to know
-their own name to read their own config.
-
-
-The Fancy Mumble Docker image also auto-derives `MUMBLE_CONFIG_*`
-environment variables into INI keys. Anything under
-`MUMBLE_CONFIG_PLUGIN_FANCY_LIVE_DOC_PORT=64740` becomes
-`plugin.fancy-live-doc.port=64740`.
-
-
-## Enabling and disabling
-
-Every Fancy plugin defaults to **disabled**. The plugin host scans the
-plugin directories and discovers every cdylib it finds, but it only
-calls a plugin's `on_load` hook after reading
-`plugin..enabled` from `mumble-server.ini` and confirming the
-value is `true`, `1`, `yes`, or `on` (case-insensitive, leading/trailing
-whitespace ignored). Disabled plugins:
-
-- never run `on_load`, so they bind no ports and start no background
- tasks;
-- do not appear in the `PluginRegistry` broadcast (wire ID 201), so
- clients do not advertise capabilities for them;
-- do not receive any lifecycle events (`on_client_connected`,
- `on_client_disconnected`, `on_plugin_data`, `on_plugin_message`).
-
-The check lives entirely in the host -- individual plugins do not (and
-should not) inspect `enabled` themselves. To turn a plugin off without
-removing the cdylib:
-
-
-1. Set `plugin..enabled=false` in `mumble-server.ini` (or simply
- remove the line; absent counts as disabled).
-2. Send `SIGHUP` to `murmurd` (or restart the container) to reload
- the configuration.
-3. Confirm the plugin's "plugin loaded" log line is replaced by a
- "plugin discovered but not enabled" line.
-
-
-When a disabled plugin is discovered the host logs:
-
-```text
-INFO mumble_plugin_host::host: plugin discovered but not enabled; set plugin.fancy-live-doc.enabled=true to load plugin=fancy-live-doc
-```
-
-## Verifying that a plugin loaded
-
-On startup the host logs each plugin it tries to load:
-
-```text
-INFO mumble_plugin_host::host: plugin host initialising dir_count=2 dirs=["/usr/lib/mumble-server/plugins", "/etc/mumble/plugins"]
-INFO mumble_plugin_host::host: plugin loaded plugin=fancy-live-doc version=0.1.0 path=/usr/lib/mumble-server/plugins/libmumble_live_doc.so
-INFO mumble_plugin_host::host: plugin loaded plugin=fancy-file-server version=0.1.0 path=/usr/lib/mumble-server/plugins/libmumble_file_server.so
-INFO mumble_plugin_host::host: plugin host ready loaded=2
-```
-
-After a successful boot you can also inspect the registry from any
-connected client. Open the **Server Info** dialog (toolbar -> "i"
-icon) and switch to the **Developer** tab. The "Plugins" panel lists
-every entry from the `PluginRegistry` envelope along with the
-`debug_rows` each plugin advertises (listening ports, snapshot
-intervals, queue depths, ...).
-
-## Built-in plugins
-
-The official `fancy-mumble/mumble-server` Docker image ships with two
-plugins out of the box.
-
-
-
-
-A self-contained HTTP file server that backs avatars, message
-attachments, and live-doc snapshots. Listens on port `64739` by
-default. Storage lives at `plugin.fancy-file-server.storagePath`,
-which **must** be a writable mount if you want files to survive
-container restarts.
-
-See [File server](/server/features/file-server/) for the full
-configuration reference and storage layout.
-
-
-
-
-Real-time collaborative documents (Yjs CRDT over WebSocket). Listens
-on port `64740` by default. Requires the `fancy-file-server` plugin
-for revision persistence; without it, documents live only in memory
-and disappear when the room tears down.
-
-See [Live documents](/server/features/live-doc/) for the user-facing
-feature description.
-
-
-
-
-## Adding a third-party plugin
-
-To run a plugin you built yourself (or grabbed from a third party):
-
-
-1. Copy the `*.so` file to a host directory you control, e.g.
- `./my-plugins/libmy_plugin.so`.
-2. Mount that directory into the container at `/etc/mumble/plugins`:
-
- ```yaml
- services:
- mumble:
- image: fancy-mumble/mumble-server:latest
- volumes:
- - ./mumble-server.ini:/data/mumble-server.ini:ro
- - ./my-plugins:/etc/mumble/plugins:ro
- ```
-
-3. Add a `plugin..enabled=true` line to `mumble-server.ini`
- along with whatever else your plugin's `from_context` parser
- expects.
-4. Restart the container and watch the boot log for
- `plugin loaded plugin=`.
-
-
-
-Third-party plugins are tied to a specific `PLUGIN_ABI_VERSION`. If
-the host bumps the ABI (currently `2`) you will see
-`abi_version mismatch` in the load log and the plugin will be skipped
-until you rebuild it against the matching `mumble-plugin-api` release.
-
-
-## Permissions and rate limiting
-
-Inbound `PluginMessage` envelopes from clients are subject to the
-same per-user rate limit as any other Mumble control message. The
-server stamps `sender_session` and `sender_name` before handing the
-message to the target plugin; the plugin cannot spoof either.
-
-Each plugin can additionally check whether the sender has a specific
-permission on a channel via `PluginContext::has_permission` - useful
-for gating administrative operations on standard Mumble ACL flags
-(`Write`, `Move`, `Kick`, ...). The flag values are defined in the
-`mumble_plugin_api::permissions` module.
-
-## See also
-
-- [Server Plugins (admin panel)](/admin/server-plugins/) — enable, disable, and uninstall plugins from the Fancy Mumble client UI without editing config files.
-- [Marketplace](/admin/marketplace/) — install community plugins with one click from the built-in marketplace.
-- [Plugins (user view)](/users/plugins/) — how end users grant or revoke trust for plugin UI surfaces (slash commands, modals, components).
+See the [plugin host implementation and plan](https://github.com/Fancy-Mumble/starling/blob/main/docs/PLUGIN-HOST-PLAN.md), [Starling configuration reference](https://github.com/Fancy-Mumble/starling/blob/main/examples/reference.toml), and [feature availability](/server/features/).
diff --git a/src/content/docs/server/upgrade.mdx b/src/content/docs/server/upgrade.mdx
index 69b2274..bc0b5b7 100644
--- a/src/content/docs/server/upgrade.mdx
+++ b/src/content/docs/server/upgrade.mdx
@@ -21,13 +21,6 @@ docker compose logs gateway
If you use a local build, update the source and run Docker Compose with --build instead. Keep the data volume mounted across restarts.
-## Move from the C++ server
+## Migrate an existing deployment
-Starling includes migration commands for the old mumble-server.ini and murmur database. Preview the database import before writing:
-
-~~~sh
-starling migrate-config /path/to/mumble-server.ini > starling.toml
-starling migrate-db --from sqlite:/path/to/murmur.sqlite --dry-run
-~~~
-
-Review the reported differences, then follow the [Starling migration guide](https://github.com/Fancy-Mumble/starling/blob/main/docs/PROTOCOL-MIGRATION.md) for the target instance, import, and verification steps. Keep the old database and configuration backup until you have tested client login, channels, ACLs, and files on Starling.
+Follow [Migrating to Starling](/server/migrating-to-starling/) for configuration conversion, database import, and the checks to complete before switching clients.
diff --git a/src/content/docs/server/wizard.mdx b/src/content/docs/server/wizard.mdx
index 28a1845..fd734d7 100644
--- a/src/content/docs/server/wizard.mdx
+++ b/src/content/docs/server/wizard.mdx
@@ -6,5 +6,3 @@ description: Start Starling and find its generated administrator credentials.
Starling starts with working defaults. On its first run it writes a starter config, creates the server certificate and SuperUser account, and prints the generated password. Save that password and keep the data directory or Docker volume.
For a local binary, run Starling in all-in-one mode and connect to localhost:64738. For Docker, follow [Run Starling with Docker](/server/docker/). Then adjust starling.toml using [Configuration](/server/config/).
-
-The Python terminal and graphical setup wizard in mumble-docker configures the older C++ server image. It does not generate Starling's TOML configuration. If you are maintaining that deployment, see the [legacy Docker repository](https://github.com/Fancy-Mumble/mumble-docker).
diff --git a/src/content/docs/troubleshooting/connection.mdx b/src/content/docs/troubleshooting/connection.mdx
index 86b1901..8f24cc6 100644
--- a/src/content/docs/troubleshooting/connection.mdx
+++ b/src/content/docs/troubleshooting/connection.mdx
@@ -34,6 +34,6 @@ Public directory registration is off until the registry fields are configured in
## The server exits at startup
-Read the gateway or affected service logs. Starling rejects unknown TOML keys. Compare your file with its [configuration reference](https://github.com/Fancy-Mumble/starling/blob/main/examples/reference.toml). The C++ server's MUMBLE_CONFIG_* variables are not Starling settings.
+Read the gateway or affected service logs. Starling rejects unknown TOML keys. Compare your file with its [configuration reference](https://github.com/Fancy-Mumble/starling/blob/main/examples/reference.toml).
If the problem persists, include the client and server versions and relevant logs in a [bug report](/troubleshooting/reporting-bugs/).
diff --git a/src/content/docs/troubleshooting/debug-logging.mdx b/src/content/docs/troubleshooting/debug-logging.mdx
index fe696af..1f2c269 100644
--- a/src/content/docs/troubleshooting/debug-logging.mdx
+++ b/src/content/docs/troubleshooting/debug-logging.mdx
@@ -50,9 +50,7 @@ debug: voice frame dropped: jitter buffer late by 47ms (threshold 30ms), last 3
-
- Screenshot placeholder: advanced settings with the log level dropdown set to debug.
-
+
## App: where is the log file?
@@ -116,30 +114,7 @@ The folder also contains rotated archives:
## Server: enable verbose logging
-
-
- ```yaml
- environment:
- MUMBLE_VERBOSE: true
- ```
- Restart the container. Logs now include `info` and `debug` lines.
-
-
- Mumble has separate log channels per plugin. Set in your custom
- INI:
-
- ```ini
- plugin.webrtc-sfu.logLevel=debug
- plugin.file-server.logLevel=debug
- plugin.link-previews.logLevel=debug
- plugin.push-fcm.logLevel=debug
- plugin.persistent-chat.logLevel=debug
- ```
-
- Useful when one feature misbehaves and you do not want the noise
- of all the others.
-
-
+Starling services use Rust tracing. Set `RUST_LOG=info` for normal operation or `RUST_LOG=debug` while investigating a problem, then restart the affected service. Apply the variable to the service you are diagnosing in your Compose environment. Return to the normal level after collecting the relevant logs.
## Server: where to read the log
diff --git a/src/content/docs/troubleshooting/reporting-bugs.mdx b/src/content/docs/troubleshooting/reporting-bugs.mdx
index 1d30a8c..09b7f2a 100644
--- a/src/content/docs/troubleshooting/reporting-bugs.mdx
+++ b/src/content/docs/troubleshooting/reporting-bugs.mdx
@@ -16,7 +16,6 @@ hitting **Submit**.
|-------------------|---------------|
| The app crashes, misrenders, or behaves wrong | [FancyMumble, app](https://github.com/Fancy-Mumble/FancyMumble/issues) |
| Starling crashes, rejects a valid config, or its Compose deployment fails | [Fancy-Mumble/starling](https://github.com/Fancy-Mumble/starling/issues) |
-| The older C++ server or its Docker image fails | [Fancy-Mumble/mumble-server](https://github.com/Fancy-Mumble/mumble-server/issues) or [mumble-docker](https://github.com/Fancy-Mumble/mumble-docker/issues) |
| The docs (this site) | [Fancy-Mumble/docs](https://github.com/Fancy-Mumble/docs/issues) |
When in doubt, file on the app repo. A maintainer will move it.
diff --git a/src/content/docs/troubleshooting/screen-share.mdx b/src/content/docs/troubleshooting/screen-share.mdx
index c6bbffd..82256f9 100644
--- a/src/content/docs/troubleshooting/screen-share.mdx
+++ b/src/content/docs/troubleshooting/screen-share.mdx
@@ -7,6 +7,5 @@ If the share does not start, first check the sender's operating-system capture p
If the sender sees a preview but viewers see a black frame or no stream, check the Starling screenshare service logs. The relay needs a public_url with a literal IP and UDP port reachable by viewers; the corresponding UDP listener must be open in the host and cloud firewalls. The [Starling example TOML](https://github.com/Fancy-Mumble/starling/blob/main/starling.example.toml) shows the setting. A private address or localhost will fail for remote viewers.
-The legacy C++ server's WEBRTCSFUPUBLICIP setting and fixed UDP port 10000 are not Starling settings. The active port comes from your Starling configuration.
For diagnosis, record the client and server versions, whether the sender's local preview works, whether all viewers fail, and the relevant logs. See [Reporting bugs](/troubleshooting/reporting-bugs/).
diff --git a/src/content/docs/users/audio.mdx b/src/content/docs/users/audio.mdx
index d24f072..224d2f5 100644
--- a/src/content/docs/users/audio.mdx
+++ b/src/content/docs/users/audio.mdx
@@ -1,224 +1,36 @@
---
title: Audio configuration
-description: Every audio setting in Fancy Mumble, explained.
-sidebar:
- order: 2
+description: Devices, activation, calibration, processing, and transmission settings.
---
-import { Steps, Aside, Tabs, TabItem, Card, CardGrid, Badge } from '@astrojs/starlight/components';
-import { Icon } from 'astro-icon/components';
+Open **More, Settings, Voice** from the voice dock. This page reads and changes the audio engine's settings. If the engine is unavailable, the page reports that instead of offering controls that cannot work.
-Audio is the number one thing people fiddle with in voice chat. This
-page walks every control under **Settings, Voice**, what it does, when
-to change it, and the trade-off.
+
-
-The screenshots and control positions below show Standard. Nebula is the default for new profiles and groups some settings differently; the underlying audio options are shared.
-
+## Devices and volume
+Choose an **Input device** for your microphone and an **Output device** for speakers or headphones. **System default** follows the operating system's choice. Microphone and speaker volume sliders are separate.
-
+On supported Windows builds, **Exclusive microphone mode** takes the device directly from its driver and can prevent another application from using it at the same time.
-
-Hidden controls? Some advanced sliders only appear in **Expert mode**.
-Turn it on under **Settings, Advanced, Expert Mode**.
-
+## Activation and voice gate
-## Input and output devices
+Choose **Voice activation**, **Continuous**, or **Push to talk**. Set the push-to-talk shortcut when that mode is selected.
-Two side-by-side dropdowns at the top of the panel:
+**Auto calibrate** asks you to speak naturally for about five seconds and tunes the gate. **Manual calibrate** exposes the Open and Close markers on the level meter. The gate's close threshold and hold prevent rapid switching between transmitting and silence.
-- **Input Device** is your microphone. *System default* picks whatever
- the operating system is currently using. Pick a specific device by
- name if you have multiple mics.
-- **Output Device** is where audio is played. Same logic.
+Use **Hear yourself** to record a short sample through the current filters and listen back before a call.
-Each has a **volume slider** from 0% to 200%. Past 100% the app
-amplifies digitally, so it can clip if pushed too high.
+## Processing
-
-Some USB headsets expose two output endpoints (game and chat). Pick
-the *game* or *handset* endpoint for Mumble. The *chat* endpoint is
-mono and narrow-band on some products.
-
+**Auto gain** adjusts microphone level for consistent speech. Noise suppression offers the algorithms included in your build, including RNNoise, DeepFilterNet, OMLSA + IMCRA, and spectral subtraction where available. Choose a setting that keeps speech clear on your hardware; compare it with noise suppression off using the sample recorder.
-## Activation mode
+
-Three radio buttons decide **when you transmit**:
+## Transmission
-
-
- Mic transmits when speech is detected. Threshold driven, default.
- The noise removal runs on the input.
-
-
- Always transmit. No noise removal, no gate. Use only in studio
- rooms or for recording.
-
-
- Transmit only while a key is held. Pick the key with the **PTT
- key** recorder below.
-
-
+Higher **Quality** uses more bandwidth. **Audio per packet** trades packet overhead against added latency. **Force TCP audio** sends voice through the control tunnel when UDP is blocked; use the normal UDP path when it works.
-### PTT key recording
+Additional controls appear for advanced user modes. Choose your mode in **Settings, Advanced** before looking for expert engine and network settings.
-When push to talk is selected a **shortcut recorder** appears. Click
-it, press the key combination you want, then release. Most modifier
-combos work (`Ctrl+Alt+Z`, mouse side buttons, single keys, and more).
-The key works globally, even when the app is not focused.
-
-## Noise removal
-
-Available **only in Voice Activation mode**. Push-to-talk and
-Continuous bypass the noise remover. Pick from a dropdown:
-
-| Algorithm | Speed | Quality | Best for |
-|-----------|:-----:|:-------:|----------|
-| **DeepFilterNet** | Heavy | Best | Modern computers, keyboard or family noise |
-| **RNNoise** | Light | Very good | Most setups, default |
-| **OMLSA-IMCRA** | Medium | Good | Smooth output, AC hum |
-| **Spectral subtraction** | Very light | Basic | Steady backgrounds |
-| **None** | n/a | n/a | Studio mic in a quiet room |
-
-You can **switch live**. Test by speaking while a steady noise source
-is running (fan, AC) and compare.
-
-
-Algorithms that were not compiled into your build will not appear in
-the dropdown.
-
-
-### Expert: per-algorithm controls
-
-In Expert mode an **Advanced controls** strip appears for each
-algorithm. Common knobs:
-
-- **Attenuation limit**: how much noise the algorithm is allowed to
- remove.
-- **Noise floor adapt rate**: how fast the algorithm learns new noises.
-- **Smoothing window**: how many frames are averaged for the cleanup.
-
-The defaults are tuned per algorithm. Do not change them unless you
-can hear the difference and have time to A/B test.
-
-## Auto sensitivity
-
-A toggle that **continuously re-fits** the voice-activation threshold
-to your room. Recommended for most users.
-
-
-
-
-### Calibrate (live level meter)
-
-Hit **Calibrate** under the threshold slider. Three visual elements
-appear:
-
-- **Fill bar**: instant level.
-- **Peak indicator**: a small marker that decays over time.
-- **Threshold line**: a vertical line at the current threshold.
-
-Speak naturally, the fill should comfortably cross the line. Stay
-silent, the fill should drop below. Adjust the threshold (or turn on
-auto sensitivity) until both are true.
-
-## Audio Processing, Auto Gain
-
-| Control | Range | Default | Description |
-|---------|-------|---------|-------------|
-| **Auto Gain** toggle | n/a | On | Pushes quiet speech up to a target loudness. |
-| **Max Amplification** | 1 to 40 dB | 20 dB | Ceiling on how much the auto-gain may add. |
-
-A higher max can rescue distant speakers, but also makes background
-noise louder. Pair with a stronger noise removal algorithm.
-
-## Compression
-
-| Control | Range | Default | What |
-|---------|-------|---------|------|
-| **Quality** (bitrate) | 8 to 320 kbps | 56 kbps | Higher is better quality and more bandwidth. |
-| **Audio per packet** | 10, 20, 40, 60 ms | 20 ms | Smaller is lower latency and more packets per second. |
-
-
-On cellular or shared Wi-Fi, **40 or 60 ms** at **24 to 32 kbps** is
-the most resilient. On a wired connection bump to **96 kbps and 20 ms**
-for music-quality voice.
-
-
-## Network
-
-
-
- Voice flows over UDP for minimum latency. This is what you want.
-
-
- Sends all audio through the existing control connection. Adds a
- little latency, but **passes through any firewall that allows the
- initial connect**. Toggle this under **Voice, Network**.
-
-
-
-## Expert section
-
-Visible only when Expert mode is on.
-
-### Gate Close Ratio (hysteresis)
-
-The gate has two thresholds, open and close. The close threshold is
-the open threshold times this ratio (default 0.8). Lower ratio is
-more aggressive cut-off, and possibly clips the ends of sentences.
-
-### Hold Frames
-
-Number of frames to keep the gate open after audio drops below the
-close threshold. Each frame is the *audio-per-packet* duration.
-Increase if your *t/s/p/k* sounds get cut.
-
-### Legacy audio backend
-
-The default audio backend supports modern Linux (PipeWire and Pulse),
-Windows, and macOS. Toggle this to fall back to the legacy backend for
-niche setups. Takes effect on the next voice toggle.
-
-## Audio statistics
-
-Live packet counters per connection:
-
-```
-To Client (server sent): good late lost (n%) resync
-From Client (we received): good late lost (n%) resync
-```
-
-- **good**: packets received in order.
-- **late**: out of order, or older than the buffer head.
-- **lost**: never arrived.
-- **resync**: codec resync events after a long pause.
-
-> Sustained **more than 2% loss** is bad. Try **Force TCP** or a lower
-> bitrate.
-
-## Quick recipes
-
-
-
- Activation: Voice. Noise removal: **DeepFilterNet**. Bitrate: 64
- kbps. Frame: 20 ms.
-
-
- Activation: **Continuous**. Noise removal: None. Bitrate: **128
- kbps**. Frame: 10 ms. Use a hardware pop filter.
-
-
- Activation: Voice. Noise removal: RNNoise. Bitrate: 24 kbps.
- Frame: 60 ms. **Force TCP** on.
-
-
- Activation: Push to Talk. Noise removal: RNNoise. Bitrate: 56
- kbps. Pick a side-button mouse for the PTT key.
-
-
-
-## Still broken?
-
-Continue to [Audio problems](/troubleshooting/audio/).
+If voice fails entirely, start with [Audio troubleshooting](/troubleshooting/audio/).
diff --git a/src/content/docs/users/chat.mdx b/src/content/docs/users/chat.mdx
index 957aa66..7b25aa3 100644
--- a/src/content/docs/users/chat.mdx
+++ b/src/content/docs/users/chat.mdx
@@ -13,9 +13,9 @@ walks every feature you will use day to day.
## Example community conversation
-This illustrative Nebula-style scene shows five sample members with different avatars, names, statuses, and interests. Rin's open profile also shows a banner, role, and bio.
+This capture of the current client shows five sample members with different avatars and a community conversation. The interactive sample scene below demonstrates their banners, statuses, and bios.
-
+
The [sample profiles](/users/profile/#try-a-sample-profile) include downloadable avatars and a distinct banner for each person. [Open the illustration at full size](/screenshot-users-chat-community.png), or [try the interactive community scene](/examples/community-scene.html) to select members and see their profiles.
@@ -23,9 +23,7 @@ The [sample profiles](/users/profile/#try-a-sample-profile) include downloadable
Use the toolbar above the composer or markdown shortcuts:
-
- Screenshot placeholder: chat composer with formatting toolbar.
-
+
| Effect | Markdown | Shortcut |
|--------|----------|----------|
@@ -55,9 +53,7 @@ navigate, **Enter** to insert.
- `@everyone` mentions every member of the channel (needs permission).
-
- Screenshot placeholder: mentions popover above the composer.
-
+
## Emoji and custom emotes
@@ -103,9 +99,7 @@ Voters click an option to cast their vote. Results are visible live
to everyone in the channel.
-
- Screenshot placeholder: poll card with three options and a live vote count.
-
+
## Reactions
@@ -136,9 +130,7 @@ the channel:
-
- Screenshot placeholder: pinned-messages tray expanded with two pins.
-
+
## Read receipts
diff --git a/src/content/docs/users/file-sharing.mdx b/src/content/docs/users/file-sharing.mdx
index f3629dd..16d6452 100644
--- a/src/content/docs/users/file-sharing.mdx
+++ b/src/content/docs/users/file-sharing.mdx
@@ -1,140 +1,39 @@
---
title: Files and images
-description: Share files and images with three access modes (public link, password, session-only) and control who can download.
-sidebar:
- order: 4
+description: Stage files in chat and choose visibility, image quality, and expiry before sending.
---
-import { Steps, Aside, Tabs, TabItem, Card, CardGrid, Badge } from '@astrojs/starlight/components';
-import { Icon } from 'astro-icon/components';
+Share files from the composer when your server provides file storage. These screenshots use sample members and a flower arrangement.
-Fancy Mumble lets you share files and images right in chat. You pick
-**who can download**, and for **how long**.
+
+## Share files
-
+1. Use the attachment menu, drop files onto chat, or paste an image into the composer.
+2. Check the staged thumbnails above the composer. Add more files with **+**, or remove a tile you do not want to send.
+3. Expand the share options. Choose visibility, compressed or full image quality, and an expiry where the server supports it.
+4. Add a message if you want, then **Send**. The options apply to the whole batch of attachments.
-## How to share something
+
-
+## Visibility
-1. In the chat composer, click the **paperclip** button. You can also
- **drag-and-drop** a file onto the chat window, or **paste** an
- image from the clipboard.
+- **This channel / This conversation**: access is restricted to the conversation's audience through the server's authenticated file service.
+- **Anyone with the link**: the link can be used outside the conversation.
+- **Password**: set a password before sending and give it to recipients separately.
-2. The **Share file** dialog opens. Pick an access mode (see below),
- add an optional message, and click **Upload**.
+Availability depends on server permissions. Locked choices explain why they cannot be used. A password share cannot be sent until it has a password.
-3. The file is uploaded to the server. A progress bar shows below
- the chat. Once finished, a small attachment card appears as a
- chat message.
+## Quality and expiry
-
+Photos can offer **Compressed** and **Full quality** with their sizes. Compression choices appear only when there is a smaller image copy to use. Other files keep their original bytes.
+Where the server supports timed deletion, choose a lifetime for the share. Expired attachments display an unavailable message. Retention and upload limits come from the server; see [File storage](/server/features/file-server/).
-
+## Open and download
-## Access modes
+Images display as a gallery; select one to view it at full size. Audio and video can have inline players. Other attachments show their filename and size. Use the card's download or open control. Password shares ask for the password when needed.
-You get three modes when sharing. Pick the one that fits.
+Deleting a chat message and deleting its stored file are separate operations. File-management actions depend on the server and your permissions.
-
-
- **Anyone with the link can download.** Use for files you would be
- okay posting publicly.
-
-
- **Recipients must enter the password you set.** Share the password
- out-of-band (in chat, in a private message, in a different
- channel). Good when you want a known group to access it.
-
-
- **Only users currently connected to this server can download.**
- The link stops working when they disconnect. Default option. Great
- for ephemeral sharing.
-
-
-
-
-If your admin has restricted public and password modes for your role,
-they will be disabled in the dialog with a tooltip explaining why.
-You can still use the **session** mode in that case.
-
-
-## What kinds of files
-
-Any file works. Images and short clips get a preview card with a
-thumbnail, click to view full size. Other files show as a card with
-filename, size, and a download button.
-
-| File type | Preview |
-|-----------|---------|
-| Images (PNG, JPG, GIF, WebP, SVG, AVIF) | Inline thumbnail and lightbox |
-| Audio (MP3, WAV, Opus) | Inline player |
-| Video (MP4, WebM) | Inline player |
-| PDFs | Filename card |
-| Everything else | Filename card with size |
-
-Maximum file size is set by the server (see
-[File server](/server/features/file-server/)). The default is about
-**50 MB** per file.
-
-## Downloading
-
-- Click the attachment card to download.
-- Files are downloaded to the **Downloads panel**, accessible from
- the chat sidebar. From there you can re-open, locate on disk, or
- re-share.
-- For **password-protected** files you will be prompted for the
- password the first time. Tick **Remember** to skip the prompt next
- time on this session.
-
-
-
- Screenshot placeholder: downloads panel with three recent items.
-
-
-## Image sharing tips
-
-- **Paste from clipboard**: hit `Ctrl+V` (or `Cmd+V`) in the chat box
- to paste a screenshot or copied image. The Share dialog opens
- pre-filled.
-- **Drag a screenshot tool window** to drop a screenshot directly.
-- **Compress before sharing**: very large images can take a while to
- upload. Resize first if you do not need full resolution.
-- **Animated GIFs** stay animated. Use the GIF picker (Klipy) inside
- the composer to search for reaction GIFs.
-
-## Retention
-
-- **Public** files stay until the admin removes them or until the
- retention policy on the server expires them (default 90 days).
-- **Password** files stay until the admin removes them.
-- **Session** files are auto-removed shortly after every recipient
- has disconnected.
-
-Your server admin can adjust each of these on the server side, see
-[File server](/server/features/file-server/) for details.
-
-## Limits on a vanilla server
-
-If you connect to a server that does not have the Fancy Mumble file
-server enabled:
-
-- File sharing is **not available**, the paperclip button is hidden.
-- You can still paste **small inline images** directly into chat as
- long as the server's `imagemessagelength` allows it (usually a
- couple of MB).
-
-## Removing a shared file
-
-- Right-click your own message and pick **Delete** to remove the file
- from chat and the server.
-- If you cannot delete a file you uploaded, your admin has restricted
- the *delete* permission. Ask a moderator.
-
-## Next step
-
-Continue with [Chat features](/users/chat/) for everything else you
-can do with text.
+If file storage is unavailable, the composer reports that restriction. Small inline images may still work within the server's message limits.
diff --git a/src/content/docs/users/live-doc.mdx b/src/content/docs/users/live-doc.mdx
index afebcef..2ea6487 100644
--- a/src/content/docs/users/live-doc.mdx
+++ b/src/content/docs/users/live-doc.mdx
@@ -13,13 +13,11 @@ see each other's edits as they happen, with colored cursors showing
who is where.
-This client feature needs a compatible live-document server plugin. The checked-out Starling plugin-host porting plan still lists live-doc among the heavier plugins awaiting migration, so a new Starling deployment should not assume it is available. Ports 64740 and 64739 below describe the older C++ server deployment. Ask your admin which server and plugin versions are installed. See [Feature availability](/server/features/).
+This feature needs a compatible live-document plugin. Verify its advertised capability and deployment status with your administrator. See [Feature availability](/server/features/).
-
- Screenshot placeholder: the live document editor with the toolbar visible and a collaborator cursor.
-
+
## Open a document
@@ -169,8 +167,7 @@ current channel.
- **"Open Document" does nothing after a few seconds**: the
`FancyLiveDocInvite` reply never arrived. Ask your admin whether a
- compatible plugin is installed. On the older C++ server, also check
- whether its live-doc listener is reachable.
+ compatible plugin is installed. Check the plugin's advertised endpoint and logs.
- **Document resets on every open**: the file server or its persistence
bridge is not configured. Documents are kept in memory only; they
survive as long as at least one viewer is connected.
diff --git a/src/content/docs/users/notifications.mdx b/src/content/docs/users/notifications.mdx
index 376a7dd..b9da28a 100644
--- a/src/content/docs/users/notifications.mdx
+++ b/src/content/docs/users/notifications.mdx
@@ -12,9 +12,7 @@ Open **Settings, Notifications** to control which events trigger a
sound and how loud each one plays.
-
- Screenshot placeholder: notifications panel with the event list.
-
+
## Master toggle
diff --git a/src/content/docs/users/personalization.mdx b/src/content/docs/users/personalization.mdx
index fd016db..cffe5d8 100644
--- a/src/content/docs/users/personalization.mdx
+++ b/src/content/docs/users/personalization.mdx
@@ -1,108 +1,36 @@
---
title: Personalization & themes
-description: Themes, fonts, chat background, channel viewer style, bubble style, font size.
-sidebar:
- order: 7
+description: Themes, light and dark appearance, message layout, and chat backgrounds.
---
-import { Steps, Aside, Tabs, TabItem, Card, CardGrid } from '@astrojs/starlight/components';
-import { Icon } from 'astro-icon/components';
import ChatBackgroundExample from '../../../components/ChatBackgroundExample.astro';
-Open **Settings, Personalize** to change how the app looks. None of
-these settings touch what other people see, they are purely your own.
+Open **Settings, Personalize** to change the appearance of your client. These choices affect your own window; edit **Settings, Profile** for the styling other members see.
-
-Nebula is the default for new profiles and has its own skins and layout settings. Standard remains available and keeps the classic glass look; existing profiles retain it. Aurora is deprecated. Switch designs under Settings, Personalization, Interface design. The theme and channel-viewer options below describe Standard and can differ in Nebula.
-
+## Theme and appearance
+Choose a theme from the preview tiles. Each theme has a light and dark scheme. **Appearance** can follow your system or use a fixed **Light** or **Dark** scheme.
-
+
-## Theme
+## Messages and text
-Fancy Mumble ships with eleven themes. Click a swatch card to apply
-it instantly.
+Choose **Bubbles**, **Flat**, or **Compact** message style. **Text size** offers Small, Medium, and Large. **Compact mode** hides avatars and tightens spacing; **Always show message actions** keeps the action controls visible without hovering.
-- **Dark** (default)
-- **Light**
-- **Apprentice**
-- **Mobel**
-- **Rose**
-- **Inversa**
-- **Hearth**
-- **Macchiato**
-- **Midnight Pretenders**
-- **Ply**
-- **Guardbase**
-
-
-
-
-## Channel viewer style
-
-The left sidebar comes in three looks:
-
-
-
- Closest to traditional Mumble. Compact tree with bracket-style
- branches. Best for big servers with deep channel trees.
-
-
- Compact one-line-per-channel view. Looks like a chat-app sidebar.
-
-
- Large channel cards.
-
-
-
-## Bubble style (chat messages)
-
-Three looks for individual chat messages:
-
-- **Bubbles** (default): rounded chat bubbles, like a messenger app.
-- **Flat**: clean rows, like a classic forum.
-- **Compact**: dense, several lines fit per visible row.
+
## Chat background
-Use an image behind the chat column. The sample below shows how a dark
-overlay keeps messages readable over an illustrated wallpaper.
-
-
-
-[Download the sample chat background](/examples/sample-chat-background.png) to try it yourself. In Nebula, open **Settings, Personalize, Chat Background** and choose the image file. You can select it again from the recent-background tiles. Drag the focus point to control which part stays visible when the chat pane crops the image.
+Choose an image or video behind the conversation. The client keeps your last five backgrounds so you can switch without choosing the file again. Adjust **Blur**, **Opacity**, and **Dim**, and move the focus point to keep the subject visible when the chat pane crops the image.
-Nebula has **Blur**, **Opacity**, and **Dim** controls. Start with a low opacity or a stronger dim setting, then adjust until messages remain easy to read. Standard also offers these controls and a **Cover** or **Tile** fit choice.
+
-**Dim** darkens the image; **Opacity** controls how strongly it shows through. **Blur** softens detail and can take a moment to process at high values.
-
-
-The original image is stored locally and re-processed when you
-change blur or dim, so you can iterate without re-uploading.
-
-
-
-
-
-## Font
-
-Pick from a curated list of fonts. The default is your system font.
-
-## Font size
-
-Three sizes: small, **medium** (default), large.
-
-## Compact mode
-
-A toggle that hides avatars and tightens the chat spacing for a
-denser layout.
+
-## Resetting
+[Download the sample background](/examples/sample-chat-background.png) to try these controls. Start with low opacity or stronger dimming so text remains readable. Video backgrounds may take time to process after changing blur or dimming.
-The **Danger Zone** at the bottom of **Settings, Advanced** has a
-**Reset** button that restores everything to defaults.
+## Server list and channels
-## Next step
+Place the server list in the **Sidebar**, **Title bar**, or **Both**. The **Channel viewer** offers Flat and Modern layouts. Choose the layout that suits the size of your channel tree.
-Continue with [Keyboard shortcuts](/users/shortcuts/).
+For your name, avatar, banner, and card styling, see [Profile customization](/users/profile/).
diff --git a/src/content/docs/users/plugins.mdx b/src/content/docs/users/plugins.mdx
index e8baece..ef3fa0a 100644
--- a/src/content/docs/users/plugins.mdx
+++ b/src/content/docs/users/plugins.mdx
@@ -26,9 +26,7 @@ UI only after you have explicitly granted trust.
## The trust prompt
-
- Screenshot placeholder: trust prompt dialog for an example plugin.
-
+
When a new or updated plugin needs your consent, a dialog appears in
the middle of the screen with:
@@ -73,9 +71,7 @@ Trusted plugins can register **slash commands**. Type `/` in the chat
composer to open the command picker, which lists all commands from all
trusted plugins on the current server.
-
- Screenshot placeholder: slash command picker open with one plugin command visible.
-
+
Slash commands can accept typed arguments. Arguments are passed
positionally (space-separated) or by name using `argname=value`.
@@ -99,9 +95,7 @@ plain language during the trust prompt:
Open **Settings > Plugins** to see every plugin the server currently
advertises.
-
- Screenshot placeholder: Settings Plugins tab with two plugins, one allowed and one blocked.
-
+
Each card shows:
diff --git a/src/content/docs/users/privacy.mdx b/src/content/docs/users/privacy.mdx
index 5e45c60..fe516b5 100644
--- a/src/content/docs/users/privacy.mdx
+++ b/src/content/docs/users/privacy.mdx
@@ -27,9 +27,7 @@ You can have multiple identities. For example one for a gaming
server, one for a work group, one for an anonymous community.
-
- Screenshot placeholder: identities panel with three saved identities.
-
+
### Create a new identity
diff --git a/src/content/docs/users/profile.mdx b/src/content/docs/users/profile.mdx
index 124ad2f..24947d1 100644
--- a/src/content/docs/users/profile.mdx
+++ b/src/content/docs/users/profile.mdx
@@ -14,22 +14,18 @@ sidebar. Fancy Mumble lets you make it look the way you want.
Open it from **Settings, Profile**.
-
-The older settings screenshots below show Standard. Nebula, the default for new profiles, places **Edit** under Avatar and **Image** under Banner. Both open a crop editor and update the live profile preview.
-
-
## Try a sample profile
Pick from eight original sample themes—cats, dogs, cars, anime, manga, K-pop, comics, and flowers—to see an avatar and matching banner together. Anime has three distinct looks: vivid pop, clean visual novel, and flat cartoon demon.
-Use the **Avatar** and **Banner** download links under any profile to try a matching pair in your own profile. In Nebula, open **Settings, Profile**, choose **Edit** under Avatar and **Image** under Banner, then adjust each crop in the editor. These files are examples; you can replace them with your own images.
+Use the **Avatar** and **Banner** download links under any profile to try a matching pair in your own profile. Open **Settings, Profile**, choose **Edit** under Avatar and **Image** under Banner, then adjust each crop in the editor. These files are examples; you can replace them with your own images.
The [example community conversation](/users/chat/#example-community-conversation) shows several members using different avatars, names, statuses, and bios together.
-
+
## What you can change
@@ -73,17 +69,17 @@ The [example community conversation](/users/chat/#example-community-conversation
## Step by step: change your avatar
-The following steps use Standard's labels. In Nebula, choose **Edit** under Avatar and confirm the crop in the image editor.
+Choose **Edit** under Avatar, select an image, and confirm its crop in the image editor.
1. Open **Settings, Profile**.
-2. Click the avatar circle (or the **Edit avatar** button).
+2. Choose **Edit** under **Avatar**.
3. **Drag and drop** or **browse** for a PNG, JPG, GIF, or WebP.
4. The image editor opens. Crop to a square. The round mask is just
how it looks, the file itself stays square.
5. (Optional) Toggle **GIF** to keep the source animated.
-6. Click **Save**.
+6. Click **Apply**.
On a Fancy Mumble server the avatar is uploaded to the server's
@@ -94,20 +90,19 @@ The following steps use Standard's labels. In Nebula, choose **Edit** under Avat
-
+
## Step by step: pick a banner
The banner sits **above** your profile card.
-In Nebula, choose **Image** under Banner to upload and crop a picture. You can also choose a solid banner color. Standard uses the following flow:
+Choose **Image** under **Banner** to upload and crop a picture, or choose a solid banner colour.
-1. **Settings, Profile, Edit banner**.
-2. Drop an image, or pick a **preset gradient**.
-3. (Optional) Tweak the **vertical offset** so the focal point is
- centered.
-4. Click **Save**.
+1. Open **Settings, Profile**.
+2. Choose **Image** under **Banner** and select your picture.
+3. Adjust the crop in the image editor.
+4. Choose **Apply**.
## Writing a bio
@@ -154,7 +149,7 @@ users, vanilla Mumble shows your plain username.
-
+
## Nameplates, frames, decorations, effects
@@ -188,9 +183,9 @@ profile is copied into each. You can then edit each one independently.
| Field | Limit |
|-------|-------|
| Bio | 2000 characters |
-| Avatar crop in Nebula | 128x128, up to 100 KB after compression |
+| Avatar crop | 128x128, up to 100 KB after compression |
| Inline bio image | 400x400, around 80 KB |
-| Banner crop in Nebula | 400x150, up to 80 KB after compression |
+| Banner crop | 400x150, up to 80 KB after compression |
## Troubleshooting
diff --git a/src/content/docs/users/screen-sharing.mdx b/src/content/docs/users/screen-sharing.mdx
index 20f250f..113e415 100644
--- a/src/content/docs/users/screen-sharing.mdx
+++ b/src/content/docs/users/screen-sharing.mdx
@@ -61,9 +61,7 @@ the **stream grid** between the chat and the member list.
- **Resize** the focus view by dragging its corner.
-
- Screenshot placeholder: focused stream view with the grid above and the pop-out button.
-
+
## Multi-stream
diff --git a/src/content/docs/welcome/intro.mdx b/src/content/docs/welcome/intro.mdx
index 8a3e0de..6c2b653 100644
--- a/src/content/docs/welcome/intro.mdx
+++ b/src/content/docs/welcome/intro.mdx
@@ -5,11 +5,11 @@ description: The client, Starling server, compatibility, and feature availabilit
Fancy Mumble is a voice chat app for Windows, Linux, and Android. It uses the Mumble protocol for voice and control, with chat, profiles, screen sharing, and other features in the client.
-The current server is [Starling](https://github.com/Fancy-Mumble/starling), a Rust implementation with a gateway and separate services for voice, chat, files, and more. The earlier [C++ server fork](https://github.com/Fancy-Mumble/mumble-server) and its Docker image remain relevant to existing deployments.
+The current server is [Starling](https://github.com/Fancy-Mumble/starling), a Rust implementation with a gateway and separate services for voice, chat, files, and more. For an existing deployment, see [Migrating to Starling](/server/migrating-to-starling/).
The client can connect to a standard Mumble server for voice and basic text chat. Fancy features depend on what the server advertises and on its configuration. Some also need a plugin or a reachable media service. See [Feature availability](/server/features/) before planning a deployment around a particular feature.
-A new client profile opens in **Nebula**. Standard remains available, and existing profiles that used Standard keep it. Aurora is deprecated. Screens and menu names in this site may differ slightly between designs.
+The guides and screenshots show the current client interface.
## Start here
diff --git a/src/content/docs/welcome/tour.mdx b/src/content/docs/welcome/tour.mdx
index ab59e79..2131007 100644
--- a/src/content/docs/welcome/tour.mdx
+++ b/src/content/docs/welcome/tour.mdx
@@ -1,120 +1,32 @@
---
title: Quick tour of the UI
-description: A whirlwind walkthrough of every part of the Fancy Mumble window.
-sidebar:
- order: 2
+description: Find channels, chat, voice controls, profiles, and settings.
---
-import { Aside, Steps } from '@astrojs/starlight/components';
+The connected client has a server rail, a channel sidebar, and a conversation pane. This capture uses sample members and messages.
-
-New profiles open in Nebula. This tour and its screenshots follow the Standard design; some controls have different names or positions in Nebula. Existing Standard profiles keep their choice. Aurora is deprecated.
-
+
-This is the **main chat view** once you are connected. Three columns,
-each doing one job.
+## Servers and channels
+The narrow server rail switches between saved connections. Expand it when you want server names to stay visible. The channel sidebar includes search, channels, and the members in each room.
-
- Screenshot placeholder: full chat view with channels left, chat center, members right.
-
+Select a channel to view its chat. Use its **Join** action to enter voice. Right-click a channel for the actions your permissions allow, including channel settings and permissions.
-## 1. Channel sidebar (left)
+## Chat
-- The server name and a status pip live at the top.
-- **Channels** are folder-like containers. Clicking a channel joins
- voice and shows that channel's chat.
-- A small badge on a channel name means **persistent chat is enabled**,
- meaning your messages will still be there next time you connect.
-- Right-click a channel to **edit, link, delete, or set ACLs** (if you
- have permission).
-- Three sidebar looks are available. Switch in
- [Personalization](/users/personalization/):
- - **Classic**, closest to traditional Mumble.
- - **Flat**, compact look.
- - **Modern**, large icons and soft glass.
+Write in the composer at the bottom. The attachment menu offers the features your server supports, such as files, polls, and Watch Together. Hover or right-click a message for reply, reaction, copy, pin, and moderation actions.
-## 2. Chat (center)
+The chat header contains member, search, pinned-message, and channel-menu controls. Select a member to open their profile. Banners, avatars, status, bio, and name styling can differ for every person.
-The chat column is more than just text:
+## Voice controls
-- **Formatted text** with bold, italic, code, lists, and links.
-- Type **`@`** to mention a user or a role.
-- Type **`:`** to pick an emoji, including custom server emotes.
-- A **GIF browser** is one click away (paperclip, GIF icon).
-- **Polls** with multiple options and a live vote count.
-- **Attachments**: drop a file or paste an image to start the
- [share dialog](/users/file-sharing/).
-- **Reactions**: hover a message, click the smiley.
-- **Pin** important messages from the context menu. Pinned messages
- appear in a tray at the top.
-- **Reply** and **quote** by right-clicking a message.
-- **Read receipts** show who has read what.
-- **Mobile call controls** are an overlay bar at the bottom on Android.
+Your identity and voice controls sit at the bottom of the channel sidebar. Use the microphone and headphones controls to change your audio state. The screen-share control indicates the available delivery mode. Open **More**, then **Settings**, to configure your microphone, profile, or appearance.
-
-Selecting multiple messages (long-press on mobile, shift-click on
-desktop) reveals the **message selection bar** for bulk copy or delete.
-
+## Settings and administration
+Settings uses a searchable sidebar. **Profile**, **Voice**, **Personalize**, **Notifications**, **Privacy**, and **Language & format** are common starting points. Desktop builds also offer shortcuts and a game overlay.
-
- Screenshot placeholder: chat composer with the mentions popover open.
-
+If your identity has administration permission, server administration pages appear in the same sidebar. The server's advertised capabilities determine which pages and controls are available.
-## 3. Member panel (right)
-
-- The active speaker gets a soft green ring.
-- Click any user to open their **profile card**, with bio, frame, and
- nameplate.
-- Right-click for the **user context menu**, with private message,
- mute, deafen, kick, ban, and role-change options.
-- Edit your own profile from **Settings, Profile**.
-
-## 4. Header bar
-
-The top bar gives you global controls:
-
-- **Mute** and **Deafen** toggles.
-- **Push-to-talk indicator**, lights up when you are transmitting.
-- **Voice level meter**.
-- **Settings** icon, opens the full settings page.
-- **Admin** icon, opens the admin panel if you have permission.
-- **Activity log**, server-wide activity. Useful for moderators.
-
-## 5. Stream grid (when screen sharing)
-
-When anyone in the channel is sharing a screen, a **stream grid**
-appears between the chat and the member list. See
-[Screen sharing](/users/screen-sharing/).
-
-
-
- Screenshot placeholder: stream grid with three active streams plus the focus view.
-
-
-## 6. Settings
-
-Opens an overlay with tabs:
-
-- **Profile**, your name, avatar, banner, bio, nameplate, frame, name
- style.
-- **Voice and Audio**, devices, activation mode, noise suppression,
- gate, gain, quality.
-- **Personalization**, themes, fonts, channel-viewer style, bubble
- style, chat background.
-- **Notifications**, per-event sounds, push, focus rules.
-- **Shortcuts**, global hotkeys for push-to-talk, mute, deafen, and
- more.
-- **Privacy**, what you share with the server.
-- **Identities**, your saved identities for different servers.
-- **Advanced**, expert mode, log level, GIF key, time format,
- auto-update, reset.
-
-## What is next?
-
-
-1. [Connect to a server](/getting-started/connect/) if you have not yet.
-2. Run through [Audio configuration](/users/audio/), five minutes saves you ten future ones.
-3. Personalize your [profile](/users/profile/).
-
+Continue with [Audio configuration](/users/audio/) or [Profile customization](/users/profile/).
diff --git a/src/styles/custom.css b/src/styles/custom.css
index a8705a4..59e8d91 100644
--- a/src/styles/custom.css
+++ b/src/styles/custom.css
@@ -317,23 +317,3 @@ div.header {
flex-shrink: 0;
color: var(--sl-color-accent-high);
}
-
-/* Boxed screenshot placeholder pattern. */
-.screenshot-placeholder {
- display: flex;
- align-items: center;
- justify-content: center;
- min-height: 220px;
- border: 2px dashed var(--sl-color-gray-4);
- border-radius: 0.75rem;
- color: var(--sl-color-gray-3);
- font-style: italic;
- background:
- repeating-linear-gradient(
- 45deg,
- transparent 0,
- transparent 12px,
- rgba(126, 87, 194, 0.06) 12px,
- rgba(126, 87, 194, 0.06) 24px
- );
-}