DS Chat Desktop packages DS Chat as a desktop application. It provides installable builds for Linux, Windows, and macOS while keeping the app connected to the DS Chat release stream.

Repository metadata and release assets are refreshed from Gitea during the static site build when the build environment has access to the repository.

Repository README

DS Chat Desktop is a cross-platform Electron client for DS Chat. It displays the hosted PWA rather than bundling a separate copy of the web frontend, so normal DS Chat deployments remain immediately available to desktop users.

The desktop shell adds:

  • A Windows notification-area, Linux system-tray, or macOS menu-bar icon.
  • A distinct unread icon, unread-message tooltip, and supported OS badges.
  • Close-to-tray behavior with normal window minimizing.
  • Persistent login sessions and single-instance handling.
  • Secure external-link and permission handling for hosted web content.
  • Native message notifications while the desktop process is running.
  • The running version and a Download Latest link in the tray menu.
  • Native Windows, macOS, Flatpak, and Debian packages for x64 and ARM64 systems.

The production application ID is org.darksingularity.DSChat, and packaged builds always load https://chat.darksingularity.org.

How it works

The Electron main process owns the window, tray, security policy, and lifecycle. The renderer is treated as untrusted remote content: Node integration is disabled, context isolation and Chromium sandboxing are enabled, unexpected navigation is denied, and external HTTP(S) links open in the system browser.

The preload exposes one optional, context-isolated API:

window.dsDesktop?.setUnreadCount(unreadMessageCount)
window.dsDesktop?.roomOpened()
window.dsDesktop?.showNotification({ eventId, roomId, title, body })
const unsubscribe = window.dsDesktop?.onNotificationClick((roomId) => {
  // Navigate with the frontend router, then call unsubscribe when appropriate.
})

The main process accepts bridge messages only from the top-level frame at the configured DS Chat origin. It validates unread counts and every notification field before using them. Any nonzero unread count enables the desktop unread indicator. An indicator lit by a desktop notification, rather than by the website’s unread count, is cleared when the window gains focus or the frontend calls roomOpened(); rooms the website still reports as unread keep it lit. Notification requests cannot contain navigation URLs; clicks return only the validated room ID to the frontend. Requests are ignored while the window is visible and focused, and replayed event IDs are deduplicated for ten minutes.

The notification request contract is:

interface DesktopNotificationRequest {
  eventId: string // nonempty, at most 128 characters
  roomId: string  // nonempty, at most 128 characters
  title: string   // nonempty, at most 100 characters
  body: string    // may be empty, at most 500 characters
}

DS Chat should use its authenticated real-time message stream and existing notification preferences to decide when to call showNotification. In desktop mode it must not call PushManager.subscribe(), because Electron does not ship a Web Push delivery service. Ordinary browsers should retain the existing Web Push flow. Desktop notifications are available while the application is open or hidden in the tray, not after it has quit completely.

The website remains the authority for unread state, using its room list and existing unread_update WebSocket events. Ordinary browsers do not see or need the optional desktop bridge.

Closing the window hides it to the tray. Minimizing uses the platform’s normal taskbar or Dock behavior. Use the tray icon or Show command to restore DS Chat; use Quit to stop the background process. Linux tray behavior depends on the desktop environment’s StatusNotifierItem or legacy tray support.

Repository layout

src/                         Electron main process, preload, and tray
assets/                      Application and platform tray artwork
scripts/                     Asset generation
tests/                       Unit tests
.gitea/workflows-disabled/   CI and release workflows (disabled; see Releases)
patches/                     Coordinated ds-chat frontend integration
release/                     Generated packages; ignored by Git
dist/                        Compiled Electron JavaScript; ignored by Git

Development

Node.js 22.12 or newer and npm are required.

npm ci
npm run typecheck
npm run lint
npm test
npm run build
npm run dev

npm run dev compiles the shell and loads the production DS Chat site. To test against a local frontend or fixture server, set DS_CHAT_URL before launching:

DS_CHAT_URL=http://localhost:5173 npm run dev

The override is intentionally ignored in packaged applications. To regenerate tray assets after editing scripts/generate-assets.mjs, run npm run assets. To create an unpacked application directory for the current platform, run:

npm run pack

Output is written under release/.

DS Chat frontend integration

The coordinated, build-verified frontend change is included at patches/ds-chat-unread-bridge.patch. Apply it from the root of the ds-chat repository before distributing the desktop application:

git apply --check /path/to/ds-chat-desktop/patches/ds-chat-unread-bridge.patch
git apply /path/to/ds-chat-desktop/patches/ds-chat-unread-bridge.patch
cd frontend
npm ci
npm test
npm run lint
npm run build

Deploy the updated website first. The desktop shell can load an older deployment, but its tray cannot receive authoritative unread state until the bridge is present.

Building packages

Install dependencies with npm ci before building any target. Electron Builder downloads Electron and platform packaging tools during the first build, so the build host needs internet access. Build each platform on its native OS, as described under Releases.

Windows x64 and ARM64

Requirements:

  • Windows 10/11 or Windows Server build runner.
  • Node.js 22.12 or newer.
  • An optional Windows code-signing certificate for distributable releases.

Build both NSIS installers:

npm ci
npm run dist:win

Build only one architecture:

npm run build
npx electron-builder --win nsis --x64 --publish never
# or
npx electron-builder --win nsis --arm64 --publish never

Installers are written to release/ as DS-Chat-<version>-x64.exe and DS-Chat-<version>-arm64.exe. The assisted NSIS installer permits choosing the installation directory and creates Start Menu and optional desktop shortcuts.

For signing, expose the certificate and password only to the build process:

$env:CSC_LINK = "C:\secure\ds-chat-signing.pfx"
$env:CSC_KEY_PASSWORD = "certificate-password"
npm run dist:win

CSC_LINK may also be a base64 value or another format supported by Electron Builder. Never commit certificate material or passwords.

macOS universal

Requirements:

  • A current macOS build host with Xcode Command Line Tools.
  • Node.js 22.12 or newer.
  • Apple Developer ID credentials for signed/notarized distribution.

Build a universal application containing Intel and Apple Silicon binaries:

npm ci
npm run dist:mac

This produces DS-Chat-<version>-universal.dmg and a matching ZIP in release/.

Unsigned local test packages can be built with the same command. For a signed and notarized release, configure the standard Electron Builder environment and enable notarization:

export CSC_LINK=/secure/path/developer-id-application.p12
export CSC_KEY_PASSWORD='certificate-password'
export APPLE_ID='developer@example.com'
export APPLE_APP_SPECIFIC_PASSWORD='app-specific-password'
export APPLE_TEAM_ID='TEAMID1234'

npm run build
npx electron-builder --mac dmg zip --universal --publish never -c.mac.notarize=true

Unsigned packages must be approved manually by users (right-click, then Open) and are suitable for development and manual testing.

Linux Flatpak and Debian packages (x86_64 and ARM64)

Requirements:

  • A Linux host matching the target architecture.
  • Node.js 22.12 or newer.
  • flatpak and flatpak-builder.
  • The Flathub remote, Freedesktop 25.08 runtime/SDK, and Electron BaseApp.

Package names differ by distribution; for Debian or Ubuntu build hosts, install the native tools first:

sudo apt update
sudo apt install flatpak flatpak-builder
flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
flatpak install --noninteractive flathub \
  org.freedesktop.Platform//25.08 \
  org.freedesktop.Sdk//25.08 \
  org.electronjs.Electron2.BaseApp//25.08

Build both configured architectures only on a host/toolchain capable of both:

npm ci
npm run dist:linux

The normal and recommended approach is one native runner per architecture:

# x86_64 host
npm run build
npx electron-builder --linux flatpak deb --x64 --publish never

# ARM64 host
npm run build
npx electron-builder --linux flatpak deb --arm64 --publish never

Each native runner produces a Flatpak bundle and a .deb installer. The resulting files include release/DS-Chat-<version>-x86_64.flatpak, release/DS-Chat-<version>-aarch64.flatpak, release/DS-Chat-<version>-amd64.deb, and release/DS-Chat-<version>-arm64.deb.

Install or upgrade a local Flatpak bundle with:

flatpak install --user --reinstall release/DS-Chat-<version>-x86_64.flatpak
flatpak run org.darksingularity.DSChat

Install a Debian package on a matching architecture with:

sudo apt install ./release/DS-Chat-<version>-amd64.deb

Use the ARM64 filename on ARM64. These are standalone bundles, not a hosted Flatpak repository, so shell updates require installing a newer bundle manually.

Updates

DS Chat Desktop does not update itself, on any platform. The tray menu shows the running version and a Download Latest… item that opens the releases page in the system browser; install the newer package over the existing one. The hosted DS Chat website continues to update independently of the desktop shell.

Releases

Releases are built by hand. No Gitea Actions runners are registered, so the workflows are kept in .gitea/workflows-disabled/ where Gitea does not run them (moving them back to .gitea/workflows/ re-enables them, once runners with the labels they expect exist). Before releasing, run npm run typecheck, npm run lint, and npm test.

  1. Update package.json and the lockfile to the date-based version (YYYY.M.D), commit, push, and tag it:

    git tag -a vYYYY.M.D -m "DS Chat Desktop YYYY.M.D"
    git push origin main vYYYY.M.D
    
  2. Build each platform natively from the tagged source, as described under Building packages: Windows (npm run dist:win, both architectures), macOS (npm run dist:mac), and Linux (npm run dist:linux, both architectures; the aarch64 Flatpak runtime and BaseApp must be installed to cross-build from x86_64). The repository requires a login, so copy the source to build hosts with git archive vYYYY.M.D rather than cloning.

  3. Check the Debian package orders correctly before publishing: dpkg-deb -f <file>.deb Version must print the version without a ~, and apt-get -s install ./<file>.deb must report an upgrade (not a downgrade) over the previous release.

  4. Collect the nine artifacts (two Flatpaks, two .deb files, the x64 and ARM64 .exe installers, the combined DS-Chat-<version>.exe, and the universal macOS .dmg and .zip) and a SHA256SUMS file, then create the Gitea release for the tag and attach them.

Windows and macOS packages are unsigned unless signing credentials are supplied (see the platform sections above). Installed clients do not check for updates; users get new versions from the releases page.

The disabled workflows predate the removal of the in-app updater and still reference the deleted scripts/prepare-update-feed.mjs and update-feed publishing. Edit them before re-enabling.

Common problems

  • The window closes but DS Chat is still running: this is expected; choose Quit from the tray menu to exit.
  • No Linux tray icon: enable the StatusNotifier/AppIndicator extension or equivalent integration for the desktop environment.
  • flatpak-builder: not found: install both flatpak and flatpak-builder; installing only the Flatpak runtime is insufficient.
  • Electron reports an unsupported Node engine: use Node 22.12 or newer.
  • The unread icon never changes: deploy the companion ds-chat patch and confirm the page is loaded from the configured DS Chat origin.
  • macOS notifications fail in an unsigned build: use a signed and notarized application for production testing.

License

DS Chat Desktop is licensed under the GNU Affero General Public License v3.0 only. See LICENSE for the full license text.

Third-party package licenses

The current lockfile contains 420 npm packages, all with declared license metadata. Direct third-party packages used by this project are:

PackageUseLicense
@types/nodeNode.js type definitionsMIT
electronDesktop application runtimeMIT
electron-builderPackage and installer buildsMIT
oxlintTypeScript lintingMIT
sharpAsset generation image processingApache-2.0
typescriptTypeScript compilerApache-2.0
vitestUnit test runnerMIT

See package-lock.json for the complete transitive npm dependency set and its recorded license metadata.