# MeshChatX Documentation

---

## Overview

MeshChatX is a local-first mesh communications client on the Reticulum Network Stack. It combines LXMF messaging, LXST voice calls, NomadNet browsing, relay chat, maps, and Reticulum utilities in one app for desktop, headless servers, and mobile.

These guides are adapted from the MeshChatX application docs bundle in the [Quad4-Software/MeshChatX](https://github.com/Quad4-Software/MeshChatX) repository. The in-app **Documentation** page ships the same material for offline use.

## Start here

- [Getting started](getting-started)
- [Installation and setup](installation)
- [Reticulum interfaces](interfaces)

## Features

- [LXMF messaging](messaging)
- [Audio calls (LXST)](audio-calls)
- [Nomad Network and Mesh Server](nomad-network)
- [Tools and utilities](tools)
- [Identities, privacy, and security](identity-and-security)
- [Plugins](plugins)

## Build and platforms

- [Building from source and packaging](building)
- [Development](development)
- [Architecture and design](architecture)
- [Raspberry Pi](raspberry-pi)
- [Android (Termux)](android-termux)
- [Meta Quest (SideQuest)](quest-sidequest)
- [Linux sandboxing](linux-sandbox)

## Protocol detail

For Reticulum crypto and interface field reference, use the [Reticulum manual](https://reticulum.network/manual/) or the Reticulum tab inside MeshChatX Documentation.

---

## Getting started

MeshChatX is a local-first mesh communications client built on the Reticulum Network Stack. It combines direct messaging over LXMF, voice calls over LXST, NomadNet page browsing, relay chat, maps, and a large set of Reticulum utilities in one application you can run on a desktop, a headless server, or a mobile device.

MeshChatX is an independent fork of [Reticulum MeshChat](https://github.com/liamcottle/reticulum-meshchat). It is not affiliated with the upstream project. The website is [meshchatx.com](https://meshchatx.com). Source and releases live on [GitHub](https://github.com/Quad4-Software/MeshChatX).

## What you need to know first

Reticulum is the mesh networking layer. It handles identities, paths, interfaces, and encrypted transport between nodes. LXMF is the messaging protocol MeshChatX uses for conversations, attachments, and propagation. LXST is the telephony layer used for audio calls.

MeshChatX does not replace Reticulum. It runs Reticulum inside a Python process, exposes a web UI, and stores your per-identity data locally in SQLite.

## How the application is laid out

When you open MeshChatX you work inside a single-page web interface. The sidebar lists the main areas of the app. The **Tools** page groups diagnostics and utilities. **Settings** holds per-identity configuration. **Identities** lets you create or switch between separate cryptographic identities.

Typical first-day workflow:

1. Install MeshChatX using a method that fits your device. See **Installation and setup**.
2. Open the web UI. The default address is https://127.0.0.1:8000 when HTTPS is enabled.
3. Go to **Interfaces** and add a way to reach the mesh. A TCP client, community interface suggestion, or LoRa RNode are common starting points.
4. Wait for paths and announces to populate. Peers appear in the announces list and in feature-specific views.
5. Open **Messages** to start an LXMF conversation, or **Nomad Network** to browse a page node.

## Runtime shape

MeshChatX ships as one Python service that serves both the API and the built frontend assets.

```
Browser or Electron window
    |
    v
Vue 3 frontend (hash routes such as #/messages)
    |
    |  REST under /api/v1/*  and  WebSocket at /ws
    v
meshchatx/meshchat.py (aiohttp server)
    |
    +--> SQLite database (per identity)
    +--> LXMF router and message store
    +--> LXST telephone (when enabled)
    +--> Reticulum stack (interfaces, paths, announces)
```

The same backend code powers Docker images, Python wheels, Linux packages, Electron desktop builds, and the Android APK. Packaging differs. Behaviour is intended to stay consistent.

In a regular browser (including headless or LAN installs), MeshChatX may register a service worker that caches hashed UI assets and the app shell so repeat loads are faster and a hard refresh can still show the boot splash while the local backend restarts. Mesh messaging, identity, and API data still require the Python backend. Electron does not use this service worker path.

## Main areas of the UI

| Area               | Route                 | Purpose                                               |
| ------------------ | --------------------- | ----------------------------------------------------- |
| Messages           | /messages           | LXMF direct messaging, folders, attachments           |
| Audio calls        | /call               | LXST voice calls and voicemail                        |
| Contacts           | /contacts           | Telephone contacts and call-related entries           |
| Relay chat         | /relay-chat         | RRC hubs and rooms (when enabled in settings)         |
| Nomad Network      | /nomadnetwork       | Browse remote NomadNet pages and files                |
| Map                | /map                | OpenLayers map, offline tiles, telemetry              |
| Network visualiser | /network-visualiser | Graph view of mesh topology (sidebar Explore)         |
| Tools              | /tools              | Ping, path tools, RNCP, bots, documentation, and more |
| Settings           | /settings           | Theme, language, LXMF, telephone, security            |
| Archives           | /archives           | Versioned snapshots of Nomad pages (sidebar More)     |
| Interfaces         | /interfaces         | Add and manage Reticulum interfaces (sidebar More)    |
| Blocked            | /blocked            | Blocked destinations (sidebar More)                   |
| Identities         | /identities         | Create, import, or switch identities (sidebar More)   |
| About              | /about              | Version, health, backups (sidebar More or footer)     |
| Documentation      | /documentation      | MeshChatX guides and the Reticulum manual             |

Relay chat appears only when rrc_enabled is turned on in settings. When enabled, the Relay chat icon can show a red mention count. Messages shows unread conversation count. Calls shows unread missed-call count.

## Documentation in the app

The **Documentation** page has two tabs.

**MeshChatX** shows the guides in this bundle. They are markdown files synced from the docs/ directory in the repository and rendered offline inside the app.

**Reticulum** shows the upstream Reticulum manual as pre-built HTML. It is bundled at build time. You can upload a newer manual ZIP if you need a different version.

Use the search bar to query both sets at once. MeshChatX guide text is currently available in English. The Reticulum manual body is English. Localized landing pages exist for several languages on the Reticulum tab.

## Storage locations

| Data                  | Typical path (CLI default)                                                                     |
| --------------------- | ---------------------------------------------------------------------------------------------- |
| MeshChatX app data    | ./storage (or --storage-dir / MESHCHAT_STORAGE_DIR)                                      |
| Reticulum config      | ~/.reticulum (or --reticulum-config-dir / MESHCHAT_RETICULUM_CONFIG_DIR)                 |
| Portable bundle       | <data-dir>/storage and <data-dir>/.reticulum when using --data-dir / MESHCHAT_DATA_DIR |
| Desktop Electron data | ~/.reticulum-meshchatx and ~/.reticulum unless overridden at launch                        |
| Per-identity database | <storage>/identities/<identity_hash>/database.db                                             |
| Docker volume         | meshchatx-config mounted at /config                                                        |

Legacy upstream data may still exist under ~/.reticulum-meshchat/. Migration tooling can move you to the MeshChatX layout.

## Where to go next

- **Installation and setup** covers Docker, wheels, desktop packages, and CLI flags.
- **Building from source and packaging** covers offline builds, Dockerfile.build, and Android APKs.
- **Development** covers task/make, version sync, and adding locales.
- **Architecture and design** explains backend managers, identity scoping, and the API model.
- **LXMF messaging** and **Audio calls** describe day-to-day communication features.
- **Identities, privacy, and security** covers backups and corruption recovery.
- **Reticulum interfaces** explains how your node joins the mesh.
- Platform guides under **Platform guides** cover Raspberry Pi, Android Termux, Meta Quest, and Linux sandboxing (Firejail and Bubblewrap).

For protocol-level detail, open the **Reticulum** tab in Documentation or visit the [Reticulum manual](https://reticulum.network/manual/) online.

---

## Installation and setup

MeshChatX can be installed in several ways. All release artifacts that ship the web UI include pre-built frontend assets. You do not need Node.js on the machine that only runs the Python wheel or Docker image.

## Requirements

| Component | Version                                            |
| --------- | -------------------------------------------------- |
| Python    | 3.11 or newer (pyproject.toml)                   |
| Node.js   | 24 or newer (development and frontend builds only) |
| pnpm      | 11.1.2 (development)                               |
| UV        | Used by Taskfile and CI                            |

**Browsers for the web UI:** Safari 16.4+, Chrome 111+, Firefox 128+.

## Choose an install method

| Method                       | Frontend included | Best for                                 |
| ---------------------------- | ----------------- | ---------------------------------------- |
| Docker image                 | Yes               | Fast server setup on Linux               |
| PyPI (reticulum-meshchatx) | Yes               | Headless install without building the UI |
| Release wheel                | Yes               | Same as PyPI from a GitHub artifact      |
| Linux AppImage               | Yes               | Portable desktop on x64 or arm64         |
| Debian .deb                | Yes               | Debian and Ubuntu systems                |
| RPM package                  | Yes               | Fedora, RHEL, openSUSE style systems     |
| Electron desktop             | Yes               | Integrated desktop with bundled backend  |
| Android APK                  | Yes               | Phones, tablets, Meta Quest sideload     |
| From source                  | Built locally     | Development and custom builds            |

Release images are published to Docker Hub (quad4io/meshchatx) and GHCR (ghcr.io/quad4-software/meshchatx). Tag suffixes: none for the standard Alpine image, -hardened for Chainguard/Wolfi, -extra for Alpine plus i2pd and yggdrasil (VARIANT=extra on the same Dockerfile).

## Docker

Quick start with Compose:

```bash
docker compose up -d
```

Basic run:

```bash
docker run -d --name reticulum-meshchatx \
  -p 127.0.0.1:8000:8000 \
  -v meshchatx-config:/config \
  ghcr.io/quad4-software/meshchatx:latest
```

Hardened example with a named volume for persistence:

```bash
docker run -d --name reticulum-meshchatx \
  --restart unless-stopped \
  --init \
  --user 1000:1000 \
  --security-opt no-new-privileges:true \
  --cap-drop ALL \
  --read-only \
  --tmpfs /tmp:noexec,nosuid,size=256m \
  --tmpfs /home/meshchat:nosuid,size=64m \
  --cpus=2.0 \
  --memory=1g \
  --memory-reservation=256m \
  --pids-limit=512 \
  -p 127.0.0.1:8000:8000 \
  -v meshchatx-config:/config \
  ghcr.io/quad4-software/meshchatx:latest
```

Default Compose maps 127.0.0.1:8000 on the host to port 8000 in the container. Data persists in the meshchatx-config volume at /config.

Compose caps Docker's own json-file logs at **10 MB × 5 files** per container (logging.options). App file logs under /config already rotate separately (meshchatx.log, about 20 MB). For a bare docker run without Compose, add the same limits or the host can fill under /var/lib/docker/containers/:

```bash
docker run -d --name reticulum-meshchatx \
  --log-opt max-size=10m --log-opt max-file=5 \
  -p 127.0.0.1:8000:8000 \
  -v meshchatx-config:/config \
  ghcr.io/quad4-software/meshchatx:latest
```

Coolify and other hosts that ignore Compose logging: should set equivalent log rotation in the platform UI.

To bind a host directory instead, mount it at /config. The container runs as UID 1000. The host directory must be writable by that user.

Run only **one** MeshChatX instance per /config volume. Startup takes an exclusive storage lock so schema migration and runtime do not overlap. For Docker or Coolify, use a single replica on that volume and replace containers in a rolling stop-then-start order instead of two replicas sharing one config path.

### Public demo instance (Coolify)

For a read-only mesh showcase on [Coolify](https://coolify.io/docs/knowledge-base/docker/compose), deploy [docker-compose.demo.yml](https://github.com/Quad4-Software/MeshChatX/blob/master/docker-compose.demo.yml). For a normal (non-demo) Coolify deployment, use [docker-compose.coolify.yml](https://github.com/Quad4-Software/MeshChatX/blob/master/docker-compose.coolify.yml).

- MESHCHAT_DEMO_MODE=1 blocks outbound mesh actions and almost all API mutations.
- MESHCHAT_AUTH=1 with default showcase password demo (MESHCHAT_DEMO_AUTH_PASSWORD).
- Optional MESHCHAT_AUTH_PAGE_HINT shows custom text on the login page (for example Username: demo and Password: demo). Demo compose sets a default hint.
- MESHCHAT_ALTCHA_ENABLED=1 and a strong MESHCHAT_ALTCHA_HMAC_KEY (required in demo compose via :?). The UI uses ALTCHA widget v3 with PBKDF2/SHA-256 challenges from /api/v1/auth/altcha/challenge.
- Assign a domain with container port **8000**, for example https://meshchatx.example.com:8000.
- Do not set MESHCHAT_AUTH_BYPASS=1 on a public host.

## Python package (PyPI or release wheel)

Published on PyPI as [reticulum-meshchatx](https://pypi.org/project/reticulum-meshchatx/). Wheels include the built web assets. No Node.js on the runtime host.

```bash
pip install reticulum-meshchatx
# or
pipx install reticulum-meshchatx
# or
uv tool install reticulum-meshchatx
```

```bash
meshchatx --headless --host 127.0.0.1
```

The meshchat command is a compatibility alias for the same entry point.

From a GitHub release artifact instead of PyPI:

```bash
pip install ./reticulum_meshchatx-*-py3-none-any.whl
```

On hosts where libopus is installed but libogg is not, LXST's vendored pyogg can raise NameError: c_int_p on import. MeshChatX applies a ctypes compatibility fix at startup (the same patch Docker runs after install). Optional telephony audio still needs the usual Opus/Ogg system libraries when you use those codecs.

## From source (git clone)

HTTPS:

```bash
git clone https://github.com/Quad4-Software/MeshChatX.git
cd MeshChatX
```

Over Reticulum with rngit (git-remote-rns):

```bash
git clone rns://06a54b505bb67b25ef3f8097e8001edc/public/MeshChatX
cd MeshChatX
```

Then use Make or Task (equivalent targets):

```bash
make install
make build
make run
```

```bash
task install
task build
task run
```

## Linux AppImage and packages

**AppImage**

```bash
chmod +x ./ReticulumMeshChatX-v*-linux-*.AppImage
./ReticulumMeshChatX-v*-linux-*.AppImage
```

**Debian package**

```bash
sudo apt install ./ReticulumMeshChatX-v*-linux-*.deb
```

Adjust the filename for your architecture.

**RPM**

```bash
sudo rpm -Uvh ./ReticulumMeshChatX-v*-linux-*.rpm
```

Download the .rpm only when the release includes one. CI uploads RPM when the packaging job produces it.

## Linux desktop emoji fonts

The emoji picker uses system fonts through Electron/Chromium. Empty squares mean a color emoji package is missing. Install one and restart the app.

| Distro               | Package                                   |
| -------------------- | ----------------------------------------- |
| Arch, Artix, Manjaro | sudo pacman -S noto-fonts-emoji         |
| Debian, Ubuntu       | sudo apt install fonts-noto-color-emoji |
| Fedora               | google-noto-emoji-color-fonts           |

If glyphs still fail, run fc-cache -fv or wait until the next login. noto-fonts helps on minimal installs that lack other symbol coverage.

## From source (development)

```bash
task install
task dev
```

task dev starts the HTTPS backend on 127.0.0.1:8000 and Vite on [http://127.0.0.1:5173](http://127.0.0.1:5173). Open that Vite URL. The [Vue DevTools](https://devtools.vuejs.org/) overlay is injected for this serve only. vite build / task run never ship it (__VUE_PROD_DEVTOOLS__ is false). Set MESHCHAT_VUE_DEVTOOLS=0 to hide the overlay. Click a component in the inspector to open it in the editor (LAUNCH_EDITOR, default code).

Python breakpoints: task debug is the same stack with [debugpy](https://github.com/microsoft/debugpy) listening on 127.0.0.1:5678 (never 0.0.0.0). Run **MeshChatX: Vite + Python** from the debugger, or start task debug and attach **Backend: Attach debugpy**. task debug:wait pauses the backend until that attach happens.

A production-like run without HMR:

```bash
pnpm run build-frontend
uv run python -m meshchatx.meshchat --headless --host 127.0.0.1
```

Useful task targets include task format, task lint, task test, task test:fe:ui, and task build.

## First launch

On first run MeshChatX creates a random Reticulum identity if you do not pass one on the command line. The identity file is stored under your configured storage directory.

Open the UI at the host and port you chose. HTTPS is enabled by default with a self-signed certificate unless you pass --no-https or provide your own PEM files.

## Command-line options

Common flags and environment variables:

| Flag                     | Environment variable            | Default        | Description                                                                              |
| ------------------------ | ------------------------------- | -------------- | ---------------------------------------------------------------------------------------- |
| --host                 | MESHCHAT_HOST                 | 127.0.0.1    | Bind address                                                                             |
| --port                 | MESHCHAT_PORT                 | 8000         | HTTP or HTTPS port                                                                       |
| --no-https             | MESHCHAT_NO_HTTPS             | false          | Serve plain HTTP                                                                         |
| --ssl-cert             | MESHCHAT_SSL_CERT             | auto           | TLS certificate path                                                                     |
| --ssl-key              | MESHCHAT_SSL_KEY              | auto           | TLS private key path                                                                     |
| --headless             | MESHCHAT_HEADLESS             | false          | Do not open a browser                                                                    |
| --auth                 | MESHCHAT_AUTH                 | false          | Require HTTP basic auth for the UI                                                       |
| --reset-password       | MESHCHAT_RESET_PASSWORD       | false          | Clear the stored password hash so a new one can be set in the UI                         |
| --storage-dir          | MESHCHAT_STORAGE_DIR          | ./storage    | Application data directory                                                               |
| --public-dir           | MESHCHAT_PUBLIC_DIR           | auto/bundled   | Frontend files. Needed for source installs without bundled assets.                       |
| --reticulum-config-dir | MESHCHAT_RETICULUM_CONFIG_DIR | ~/.reticulum | Reticulum configuration                                                                  |
| --data-dir             | MESHCHAT_DATA_DIR             | none           | Portable root (storage + .reticulum subdirs when the two paths above are unset)      |
| --identity-file        | MESHCHAT_IDENTITY_FILE        | none           | Load identity from file                                                                  |
| --rns-log-level        | MESHCHAT_RNS_LOG_LEVEL        | none           | Reticulum log level                                                                      |
| (env only)               | MESHCHAT_RNS_LOG_DEST         | logging        | stdout keeps RNS on the console. With a log dir, default is the rotating Python logger |
| --auto-recover         | MESHCHAT_AUTO_RECOVER         | false          | Attempt SQLite recovery on start                                                         |
| --emergency            |                                 | false          | Start without database                                                                   |
| --disable-plugins      |                                 | false          | Disable the plugin system                                                                |

CLI flags override environment variables when both are set.

### Portable installs (removable media, Tails, USB sticks)

MeshChatX already supports relocating all persistent state off the home directory. Use either explicit paths or a single data root:

```bash
export PERSIST="/media/amnesia/Persistent/meshchatx"
mkdir -p "$PERSIST"

meshchatx --headless \
  --data-dir="$PERSIST"
```

That creates and uses $PERSIST/storage for MeshChatX (identities, SQLite, plugins) and $PERSIST/.reticulum for Reticulum interfaces and transport config. You can set the same layout with environment variables:

```bash
export MESHCHAT_DATA_DIR="$PERSIST"
meshchatx --headless
```

Equivalent explicit form (overrides any --data-dir subpaths when you set these yourself):

```bash
meshchatx --headless \
  --storage-dir="$PERSIST/storage" \
  --reticulum-config-dir="$PERSIST/.reticulum"
```

The Electron desktop app (AppImage, portable exe, macOS bundle) honors the same --data-dir / --storage-dir / --reticulum-config-dir flags (or the matching MESHCHAT_DATA_DIR / MESHCHAT_STORAGE_DIR / MESHCHAT_RETICULUM_CONFIG_DIR environment variables) on every platform, not just Windows:

```bash
export PERSIST="/media/amnesia/Persistent/meshchatx"
./MeshChatX-x86_64.AppImage --data-dir="$PERSIST"
```

On Windows portable exe builds, storage and Reticulum config also default next to the .exe when PORTABLE_EXECUTABLE_DIR is set (used by the portable target automatically), without needing any flags.

## Reticulum manual bundle

The Reticulum HTML manual is fetched from the upstream website **master** branch at build time by default (clearnet ZIP). There is no in-app clearnet refresh. After cloning the repository, or before packaging a release, run:

```bash
pnpm run build-docs
```

CI release builds use the clearnet path. Without a bundled copy the Reticulum tab may show an upload prompt until you build docs or upload a manual ZIP offline.

## Advanced: Optional RNS-only installation (pip-rns)

MeshChatX includes optional tooling to pull rns, lxmf, lxst, and the Reticulum manual from markqvist's rngit remotes over the mesh instead of clearnet.

**Note:** Installing Python packages over RNS is slower than PyPI and fits mesh-only hosts with restricted clearnet. PyPI remains the default path for CI and standard development.

| Remote                                                       | Purpose               |
| ------------------------------------------------------------ | --------------------- |
| rns://7649a50d84610232d1416b41d2896aff/reticulum/reticulum | RNS package           |
| rns://7649a50d84610232d1416b41d2896aff/reticulum/lxmf      | LXMF package          |
| rns://7649a50d84610232d1416b41d2896aff/reticulum/lxst      | LXST package          |
| rns://7649a50d84610232d1416b41d2896aff/reticulum/website   | Manual / website HTML |

This uses [pip-rns](https://github.com/Quad4-Software/pip-rns) for the Python packages and git + git-remote-rns for the docs tree. Default aliases live in scripts/pip-rns/aliases.

**Bootstrap note:** pip-rns needs a working Reticulum stack to reach the remotes. Install rns once from PyPI, a wheel, or an existing environment, then use the mesh path for updates.

```bash
# Optional: Install/update rns, lxmf, lxst into the uv environment over RNS
task deps:backend:rns

# Optional: Bundle the Reticulum manual from the rngit website remote
task docs:rns
```

Equivalent direct commands:

```bash
bash scripts/pip-rns-deps.sh
python scripts/build/fetch_reticulum_manual.py --force --via-rns
```

Set PIP_RNS_CONFIG to point at another aliases directory if needed. MESHCHATX_RETICULUM_DOCS_URL=rns://... also works for a custom website remote.

## Identity bootstrap

You can supply an identity at startup:

- --identity-file /path/to/identity
- --identity-base64 or --identity-base32 with the corresponding environment variables

Otherwise MeshChatX generates one and saves it under <storage>/identity. Additional identities are created from the **Identities** page. Each identity has its own database, LXMF router, and settings while sharing one Reticulum process.

## After install

1. Add at least one **interface** so Reticulum can reach peers.
2. Review **Settings** for display name, theme, language, and LXMF stamp costs.
3. Enable **telephone** in settings if you plan to use audio calls.
4. Open **Documentation** for MeshChatX guides and the Reticulum manual offline.

Platform-specific notes live under **Platform guides** in this documentation bundle, including **Linux sandboxing** (Firejail and Bubblewrap). Offline packaging, Android APK builds, and Dockerfile.build are in **Building from source and packaging**. Contributor task targets and locales are in **Development**.

---

## Building from source and packaging

How to build MeshChatX offline, package desktop artifacts, and produce Android APKs. For day-to-day install and CLI flags, see **Installation and setup**.

Tagged releases build Linux wheel/AppImage/deb/rpm, Windows, macOS, Flatpak, and Android APKs (when the tag is on dev or master) in [build-release.yml](https://github.com/Quad4-Software/MeshChatX/blob/master/.github/workflows/build-release.yml). The container image is [docker.yml](https://github.com/Quad4-Software/MeshChatX/blob/master/.github/workflows/docker.yml). Branch and PR Android CI is [android-build.yml](https://github.com/Quad4-Software/MeshChatX/blob/master/.github/workflows/android-build.yml). Linux x64 and arm64 AppImage and DEB are built on GitHub. RPM is uploaded when the job produces one.

## Offline builds

Two levels:

1. Cached: you already ran make install once, so node_modules, .venv, and local caches exist.
2. Air-gapped: the build machine has never had internet. Build a bundle on a networked machine and copy it over.

### Cached offline builds

Set MESHCHATX_OFFLINE_BUILD=1 before any build command. That skips micron-parser-go WASM, the Reticulum manual, and repository wheel fetches, and runs package managers offline. Missing cache files fail the build instead of hanging.

```bash
MESHCHATX_OFFLINE_BUILD=1 make install
MESHCHATX_OFFLINE_BUILD=1 pnpm run build:offline
MESHCHATX_OFFLINE_BUILD=1 pnpm run dist:linux:offline
MESHCHATX_OFFLINE_BUILD=1 ./gradlew :app:assembleRelease
```

Cached mode only skips build-time network. The first make install still needs the network, or pre-populated pnpm and uv caches.

### Air-gapped builds

On the online machine:

```bash
pnpm run bundle:offline
bash scripts/create-offline-bundle.sh --warm-packaging
tar czf meshchatx-offline-linux-x64.tar.gz -C vendor/offline meshchatx-offline-bundle-*/
```

--warm-packaging is optional. It pre-downloads tools such as appimagetool.

On the air-gapped machine:

```bash
tar xzf meshchatx-offline-linux-x64.tar.gz
bash scripts/install-offline.sh
MESHCHATX_OFFLINE_BUILD=1 make build
MESHCHATX_OFFLINE_BUILD=1 pnpm run dist:linux
```

The bundle is platform-specific (Electron, esbuild, and other native binaries). Create it on the same OS and architecture as the air-gapped host. That host still needs node, pnpm, uv, and python3. The bundle is dependencies and caches, not the toolchain.

Android is separate. The offline bundle does not include Chaquopy wheels. Build those on an online machine with bash scripts/build-android-wheels-local.sh, copy android/vendor/ next to the project, then run Gradle with MESHCHATX_OFFLINE_BUILD=1.

## Desktop packages from source

```bash
pnpm run dist:linux-x64
pnpm run dist:linux-arm64
pnpm run dist:rpm
task dist:fe:rpm
```

Windows (x64 and arm64) and macOS (arm64 and universal) scripts are in package.json for local builds.

## Container build (wheel, AppImage, deb, rpm)

[Dockerfile.build](../../Dockerfile.build) runs the same shell steps CI uses (Poetry, pnpm, task, packaging APT deps). It is aimed at linux/amd64 (NodeSource amd64 tarball, Task amd64 binary).

MESHCHATX_BUILD_TARGETS defaults to all. Other values: wheel, or electron (AppImage + deb for x64 and arm64, best-effort RPM, no wheel).

```bash
docker build -f Dockerfile.build -t meshchatx-build:local .
docker build -f Dockerfile.build --build-arg MESHCHATX_BUILD_TARGETS=wheel -t meshchatx-build:wheel .
```

Copy artifacts off the image:

```bash
cid=$(docker create meshchatx-build:local)
docker cp "${cid}:/artifacts" ./meshchatx-artifacts
docker rm "${cid}"
```

## Android APK

Native APK builds, not only Termux. From the repo root:

```bash
bash scripts/build-android-wheels-local.sh
cd android
./gradlew --no-daemon :app:assembleDebug :app:assembleRelease
```

Offline:

```bash
MESHCHATX_OFFLINE_BUILD=1 ./gradlew --no-daemon :app:assembleRelease
```

That skips the repository wheels fetch. android/vendor/ wheels and meshchatx/public/repository-server-bundled/bundled/ must already be present.

There is one Android variant. Gradle syncs the full meshchatx/ tree into app/src/main/python/meshchatx/, including the offline repository wheel bundle. Published builds are universal: one debug APK and one release APK per run, with the native ABIs from android/app/build.gradle.

- Debug: android/app/build/outputs/apk/debug/app-debug.apk
- Release: android/app/build/outputs/apk/release/ReticulumMeshChatX-v*-android-universal-unsigned.apk
- GitHub release: ReticulumMeshChatX-v<version>-android-universal.apk

Release APKs are unsigned unless you configure signing (scripts/sign-android-apks.sh). Native ABIs follow android/app/build.gradle, including armeabi-v7a when that ABI is enabled. Building those wheels needs an Android SDK on ANDROID_HOME.

If dist/reticulum_meshchatx-*.whl exists (for example from python -m build --wheel -o dist .), bundled repository refresh prefers that wheel over PyPI. CI builds that wheel before the Android Gradle step.

More: [Android (Termux)](android-termux), [android/README.md](https://github.com/Quad4-Software/MeshChatX/blob/master/android/README.md), [Meta Quest (SideQuest)](quest-sidequest).

## See also

- **Installation and setup** for Docker, wheels, and CLI flags
- **Development** for task targets and version sync
- **Linux sandboxing** for Firejail and Bubblewrap around a built binary

---

## Development

Contributor workflow: install, format, lint, test, version bumps, and adding locales. Runtime install paths are in **Installation and setup**. Packaging is in **Building from source and packaging**.

## Branches

| Branch | Purpose                                            |
| ------ | -------------------------------------------------- |
| master | Stable releases                                    |
| dev    | Active development. May be incomplete or breaking. |

## Daily commands

```bash
task install
task hooks:install   # pre-commit format/lint + commitlint (once per clone)
task format
task lint
task test
task build
```

Makefile targets call the same Taskfile commands:

| Command              | Delegates to       | Description                                                 |
| -------------------- | ------------------ | ----------------------------------------------------------- |
| make install         | task install       | Install pnpm and UV dependencies                            |
| make run             | task run           | Run MeshChatX via UV                                        |
| make build           | task build         | Build frontend and backend artifacts                        |
| make format          | task format        | Format frontend and backend                                 |
| make lint            | task lint          | ESLint, vue-tsc, knip, Ruff, basedpyright                   |
| make test            | task test          | Frontend and backend tests                                  |
| make clean           | task clean         | Remove build artifacts and node_modules                     |
| make tree-rsm-verify | (shell)            | Verify meshchatx.rsm signature and hashes                   |
| make tree-rsm-sign   | (shell)            | Sign tree inventory (needs RNS_ID_PATH)                     |
| make hooks-install   | task hooks:install | Git hooks: format/lint staged files, commitlint, RSM resign |

For a Vite HMR loop, use task dev as described in **Installation and setup**.

## Lockfiles and install scripts

From a clean clone:

```bash
git clone https://github.com/Quad4-Software/MeshChatX.git
cd MeshChatX
corepack enable
pnpm config set verify-store-integrity true
pnpm install --frozen-lockfile
pip install "uv==0.11.15"
uv lock --check
uv sync --group dev
pnpm run build-frontend
uv run python -m meshchatx.meshchat --headless --host 127.0.0.1
```

pnpm install --frozen-lockfile fails if pnpm-lock.yaml does not match package.json, so an unexpected upstream version cannot land silently. Store integrity is also on in pnpm-workspace.yaml. The extra pnpm config set line hardens the user-level config too.

pnpm v11+ blocks lifecycle scripts by default. Only packages listed under allowBuilds in pnpm-workspace.yaml may run install scripts (electron, electron-winstaller, esbuild). uv lock --check fails if uv.lock is out of date with pyproject.toml. uv sync then installs from the lockfile only. Pin UV with pip install "uv==0.11.15" to match CI.

To update dependencies on purpose, run pnpm update or uv lock in its own commit and read the lockfile diff before you push.

## Versioning

Edit the version field in package.json, then run pnpm run version:sync (also the first step of pnpm run build). That copies the number into pyproject.toml, the Python version modules, Android Gradle, electron/app-version.json, the README and translated READMEs, the Raspberry Pi pipx example, Arch PKGBUILD helpers, third-party notices, and GitHub issue-template placeholders.

Changelog entries are still written by hand when you cut a release. meshchatx.__version__ is read from meshchatx/src/version.py without importing meshchatx.src, so import meshchatx stays lightweight.

## Adding a language

Locale discovery is automatic. Add a file under meshchatx/src/frontend/locales/ (for example xx.json) with the same keys as en.json and a top-level _languageName string for the selector label. Copy en.json and translate the values. Machine-assisted generation is optional.

For a machine-generated first draft from en.json, use scripts/argos_translate.py. It keeps interpolation variables such as {count} intact.

```bash
pipx install argostranslate
python scripts/argos_translate.py --from en --to xx --input meshchatx/src/frontend/locales/en.json --output meshchatx/src/frontend/locales/xx.json --name "Your Language Name"
```

After a machine pass, have an LLM or a human check grammar, context, and tone.

```bash
pnpm test -- tests/frontend/i18n.test.js --run
```

That checks key parity with en.json. No other code changes. The app, language selector, and tests pick up locales from meshchatx/src/frontend/locales/ at build time.

Translation fixes are welcome via LXMF (f489752fbef161c64d65e385a4e9fc74) or a pull request.

In-app MeshChatX guides under docs/en/ are English today. Localized landing pages exist for the Reticulum manual tab.

## See also

- **Architecture and design** for process layout and managers
- **Building from source and packaging** for offline and APK builds

---

## Architecture and design

MeshChatX is a fork of Reticulum MeshChat with LXST telephony, RRC relay chat, Nomad tooling, plugins, and a SQL backend without Peewee. The goals below shaped how the codebase is organized.

## Design goals

- Keep a local-first runtime that works on desktop, mobile, containers, and single-board computers.
- Preserve Reticulum and LXMF semantics while improving usability and operational tooling.
- Support multiple identities in one process without cross-identity data leakage.
- Keep the Python backend and Vue frontend independently testable.
- Run in constrained environments with predictable SQLite behaviour.

Mesh features should follow Reticulum post-IP design patterns: portable identity hashes, announces, store-and-forward, transport-agnostic APIs, and scarce payloads. See the [Zen of Reticulum](https://reticulum.network/manual/zen.html).

## Process overview

One Python process owns the web server, Reticulum stack, and all per-identity managers. The Vue frontend is static assets served from meshchatx/public/ after a Vite build.

```
ReticulumMeshChat (meshchat.py)
    |
    +-- HTTP package (src/backend/http/)
    |       +-- middleware + register_all_routes
    |       +-- routes/* (/api/v1/*, shell, static helpers)
    |       +-- ws/* (dispatch and handlers)
    +-- IdentityContext (per active identity)
    |       +-- SQLite via database layer
    |       +-- LXMRouter
    |       +-- TelephoneManager (LXST)
    |       +-- Domain managers (messages, map, docs, RRC, ...)
    +-- Shared Reticulum instance (~/.reticulum by default)
```

Optional **Electron** wraps the same backend binary and loads the UI from the local HTTPS server.

## Application shell

ReticulumMeshChat in meshchatx/meshchat.py is the orchestration layer. It wires HTTP via meshchatx/src/backend/http/, starts and stops identity contexts, wires crash recovery, and coordinates shared process concerns.

Path helpers live in meshchatx/src/path_utils.py, ssl_self_signed.py, and env_utils.py. meshchat.py re-exports them for compatibility.

## Identity-scoped context

IdentityContext in meshchatx/src/backend/identity_context.py encapsulates everything tied to one cryptographic identity:

- Storage under storage/identities/<identity_hash>/
- Identity-local SQLite database (schema version tracked in migrations)
- LXMF router state and propagation directories
- Manager instances for messages, announces, docs, maps, forwarding, bots, RRC, Nomad page nodes, and more

Switching identities tears down the old context and loads another. Global mutable state that could leak between identities is avoided by design.

## Destination aspects

Mesh peers are addressed by destination hash plus aspect. MeshChatX currently uses:

| Aspect              | Role                         |
| ------------------- | ---------------------------- |
| lxmf.delivery     | LXMF mail                    |
| lxmf.propagation  | Propagation node             |
| lxst.telephony    | LXST calls                   |
| nomadnetwork.node | NomadNet / Mesh Server pages |
| rrc.hub           | Relay Chat hub               |
| map-data-v1       | Published map overlay packs  |

map-data-v1 announce app_data is a short JSON label and file count. Catalog and file bytes travel over an RNS Link, not announce payloads or LXMF. The destination is created and announced only after at least one pack is published. Mesh announce stays a separate opt-in.

## Manager-centric domain logic

Feature behaviour lives in modules under meshchatx/src/backend/. Examples include message handling, announce trimming, documentation, maps, page nodes, telemetry, interfaces, forwarding aliases, and RN-specific tool handlers.

meshchat.py should stay focused on transport and lifecycle. Business rules belong in managers where they can be unit tested.

## Persistence

- **Engine:** SQLite with explicit SQL and migrations (no ORM).
- **Schema:** Versioned migrations run during startup and identity setup.
- **Backups:** Automatic and manual database backups under database-backups/.
- **Recovery:** --auto-recover, emergency mode, and Electron crash UI can restore from backups.

## HTTP API

Routes are registered through backend/http/register.py into aiohttp route tables
(still discoverable as @routes.<method> for contract scanners). Categories include:

- Application status and configuration
- Authentication and session management
- LXMF messaging and conversations
- Telephone and voicemail
- Interfaces and Reticulum configuration
- Nomad Network and page nodes
- RRC client and server
- Tools (ping, RNPath, RNCP, RNSH, translator, bots)
- Map overlays and map-data-v1 publish/discover
- Documentation and maintenance

The frontend uses fetch via apiClient.js with CSRF tokens on mutating requests.

## WebSockets

The UI connects to /ws for low-latency updates. Event types include new LXMF messages, identity switches, telephone state, RRC activity, Nomad download progress, RNCP transfers, and plugin events. Handlers are registered in wsEventRegistry.js and dispatched through wsEventBridge.js.

Audio calls can use /ws/telephone/audio for browser-side codec bridging.

## Security model

MeshChatX defaults toward secure local operation:

- HTTPS and WSS enabled by default.
- Self-signed certificates generated per identity when custom PEM files are absent.
- Optional HTTP basic authentication (--auth).
- Encrypted session cookies via aiohttp_session.
- CORS, CSP, and defensive middleware on HTTP responses.
- Access attempt logging with lockout when auth is enabled.

The project includes extensive automated tests around auth and sessions. Even so, exposing MeshChatX directly to the public internet is not recommended without additional hardening.

Password reset is available with --reset-password or MESHCHAT_RESET_PASSWORD=true, which clears the stored bcrypt hash so you can set a new password in the UI.

## Build and packaging

One source tree produces:

- Development runs via uv run python -m meshchatx.meshchat
- Python wheels with bundled public/ assets
- Container images (Alpine Dockerfile with standard/extra VARIANT, plus hardened Chainguard)
- Electron builds for Windows, macOS, and Linux
- Android APK via Chaquopy

Frontend build output always lands in meshchatx/public/ so runtime behaviour matches across targets.

## Reliability features

- Crash recovery integration in Electron and backend startup checks
- Database integrity verification
- Backup, restore, and snapshot APIs
- Explicit teardown when switching identities or shutting down forwarding resources
- Health and status endpoints suitable for container probes

## Extensibility

MeshChatX supports plugins with separate frontend and backend runtimes:

- **Contribution registries** under meshchatx/src/frontend/js/registries/ for navigation, tools, commands, settings, and WebSocket events.
- **Frontend plugins** run in dedicated Workers (PluginHost.js) with declarative UI slots.
- **Backend WASM plugins** run in wasmtime with fuel metering and capability-gated host functions.
- **Backend Python plugins** (backend.type: "python") run in-process with a permission-checked host (log, managers, storage, network flag).
- **WASM bundles** embed manifest/files/signature in custom sections and unpack on install.
- **Sideband-compatible loader** optionally execs flat *.py plugins with PLUGIN_COMMAND LXMF dispatch.
- **Security core** verifies RSG signatures, trusted publishers, integrity hashes, and heuristic findings.
- **HTTP API** under /api/v1/plugins/* and /api/v1/sideband-plugins/* for install, enable, invoke, trust, and Sideband config.

Practical extension paths today:

- Plugin manifests with contributes and permissions blocks
- New API routes and manager modules
- Frontend pages wired through registries
- New settings via ConfigManager and CLI or environment variables
- Database schema changes through migrations
- Generic RNS Link transport over WebSocket (rns.link.*) for external consoles and plugins (see [RNS Link API](rns-link-api))

Granted plugin manager capabilities include destinationPath.read, debugLog.read, bugReport.*, and rnsLink.open / identify / request / send / close. Hooks include announce.received and rns.link.event. Storage (storage:isolated) and outbound HTTP (network:fetch) are also grantable. The installation preview scans plugin files for external URLs and stores the user-selected grant subset.

When adding features, prefer identity-scoped state, explicit migrations, endpoint tests, and narrowly declared plugin permissions.

## NomadNet and Mesh Server

The Nomad browser and Mesh Server (page nodes) share a rendering pipeline for Micron, Markdown, plain text, and sanitised HTML. Authoring rules are documented in **NomadNet page formats**.

## Related reading

- **Getting started** for UI navigation and first steps.
- **LXMF messaging**, **Audio calls**, and **Reticulum interfaces** for feature behaviour.
- [Plugins](plugins) for extension architecture and security.
- The **Reticulum** tab in Documentation for protocol reference.

---

## LXMF messaging

MeshChatX uses LXMF (LXMF Message Format) for direct and store-and-forward messaging over Reticulum. Each identity has an LXMRouter registered under aspect lxmf.delivery.

## Conversations

Open **Messages** to see your conversation list. Each row is a peer destination you have exchanged traffic with or selected from announces.

The Messages sidebar icon shows a red count when you have unread LXMF conversations. Opening a thread marks it read. If a new message arrives while that thread is already open, it is marked read without needing to switch away and back.

From a conversation you can:

- Send and receive text messages
- Attach images, audio clips, and files
- Reply with quotes and add reactions
- Organise threads into folders and pin important chats
- Run bulk operations on multiple conversations

Incoming messages arrive over the WebSocket as lxmf_message events. The UI updates without a full page reload.

## Attachments and rich content

The composer supports:

- **Images** via LXMF image fields
- **Audio** via LXMF audio fields
- **Files** as LXMF file attachments
- **Stickers and GIFs** when enabled in settings
- **User icons** stored as LXMF app data

Large payloads follow LXMF sizing and stamp rules configured in settings.

## Propagation nodes

When a peer is not reachable directly, LXMF can store messages on propagation nodes.

MeshChatX can:

- Run a **local propagation node** on your identity
- **Sync** with remote propagation nodes you trust
- **Auto-select** a preferred propagation peer via AutoPropagationManager from local lxmf.propagation announces and RNS paths (small sync probe, remembered destination hashes, no central directory)
- **Retry** failed direct deliveries through propagation when configured

Manage nodes from **Tools -> Propagation nodes** or related settings entries.

## Stamp costs and stranger protection

LXMF uses work proofs (stamps) to limit abuse. Settings let you tune:

- Outbound stamp costs for your messages
- Inbound stamp requirements for unknown senders
- **Stranger protection** options such as blocking strangers, attachments, or links from unknown peers
- **Flood protection** with dynamic inbound stamp costs based on rate

Raise inbound costs when you operate a public-facing node. Lower them on trusted private meshes.

## Filtering and blocking

- **Blocked** destinations stop traffic from specific hashes.
- **Sieve filters** (beta) drop inbound messages by pattern.
- **Message blocklist** (beta) complements sieve rules for known bad content.
- **Spam reporting** helps you mark unwanted conversations.

## Paper messages

**Tools -> Paper message** generates LXMF URIs you can share as QR codes. Another MeshChatX user can ingest the URI to receive the payload. Useful for offline handoff when no live path exists yet.

## Forwarding

ForwardingManager supports alias identities that forward messages between peers according to rules you define. Configure forwarding from **Tools -> Forwarder**.

## Import and export

You can import and export messages and folder structures for backup or migration. Operations go through the API and respect identity boundaries.

## Local retention

**Local message auto-delete** removes old messages after a configured retention period. Tune this in settings if you operate on storage-constrained hardware.

## Messaging flow

```
Composer in UI
    |
    v
POST /api/v1/lxmf-messages/send
    |
    v
LXMRouter (identity-local)
    |
    +--> Direct path to peer destination
    |
    +--> Propagation node (when direct delivery fails or policy requires it)
    |
    v
Peer LXMF router
    |
    v
WebSocket lxmf_message event on recipient UI
```

## Tips

- Set a **display name** in settings so announces show a friendly label.
- Enable **auto-announce** so your lxmf.delivery aspect stays visible on the mesh.
- Check **Interfaces** if messages stall. No path to the peer means LXMF cannot deliver.
- Review stamp settings before joining busy public meshes.
- While a large message is downloading, the header shows **Cancel incoming**. That stops active LXMF delivery resource transfers (cancel_all_inbound / per-resource cancel). Outbound send cancel stays on each message menu.

## See also

- **Reticulum interfaces** for connectivity
- **Identities, privacy, and security** for auth and HTTPS
- Reticulum manual section on LXMF for protocol detail

---

## Audio calls (LXST)

MeshChatX uses LXST for voice telephony over Reticulum. Telephone functionality is optional and controlled per identity in settings.

## Enable telephony

Turn on **telephone** in settings before using the **Call** page. MeshChatX announces your callable destination under aspect lxst.telephony when announcing is enabled.

Peers who announce the same aspect appear as callable contacts.

## Placing and receiving calls

From **Call** or a contact entry you can:

- **Dial** another identity by hash
- **Answer** or **decline** inbound rings
- **Hang up** an active session
- **Mute** microphone (transmit) or speaker (receive)
- Switch **full duplex** or **half duplex** during a live call
- Use **push-to-talk** while in half duplex (hold the PTT control or Space)

Half duplex uses LXST packetizer squelch so idle airtime stays low on constrained links. Full duplex keeps both directions open.

Call state changes arrive over the WebSocket (telephone_ringing, telephone_call_established, telephone_call_ended, and related events).

While connected, the Call screen shows link stats (packets, bytes, approximate bitrates, path hops, and interface).

## Audio path

The frontend loads Codec2 assets for voice encoding (Codec2Loader.js). Browser and Electron builds use a Web Audio bridge at /ws/telephone/audio. Packaged desktop builds bundle the backend that negotiates LXST sessions.

## Voicemail

When you miss a call, voicemail may be offered depending on settings:

- Record a custom greeting
- Upload or generate greeting audio
- Play back messages left for you

Voicemail events surface as new_voicemail on the WebSocket.

## Call history and recordings

The **Call** area keeps history of placed, received, and missed calls. You can record calls when the feature is enabled and policy allows storage on your device.

Unread missed calls show as a red count on the Calls sidebar icon and the header phone button. Opening the Call page clears that count. Desktop and Android still show a one-shot missed-call notification when the event happens.

## Ringtones

Upload custom ringtones and assign them per contact. Default sounds are used when no override exists.

## Do not disturb and contacts-only

Settings support:

- **Do not disturb** to silence inbound rings
- **Contacts-only** mode to reject calls from unknown hashes

Combine these with the **Blocked** list for finer control.

## Telephone contacts

Import and export telephone contacts separately from LXMF conversation peers. Contacts drive caller display names and ringtone overrides.

## Call setup flow

```
Caller UI: initiate call
    |
    v
GET /api/v1/telephone/call/{identity_hash}
    |
    v
LXST Telephone session over Reticulum
    |
    +--> Signalling and media via LXST
    |
    +--> /ws/telephone/audio (browser audio bridge)
    |
    v
Callee UI: ring, answer, or decline
```

## Windows microphone (Electron, Windows 10 / 11)

Calls and voice attachments use the mic through Chromium. If the UI has no access or getUserMedia fails, check Windows privacy first. That is a common miss for Win32 apps, Electron included.

1. Win+R, paste ms-settings:privacy-microphone, Enter.
2. Turn Microphone access on.
3. Enable Let desktop apps access your microphone (wording varies by Windows version).
4. If a per-app list appears, make sure MeshChatX is not denied.

Also check Settings, System, Sound so the app is not muted and a working input device is selected.

## Tips

- Verify **Interfaces** and paths before troubleshooting audio quality. Packet loss on the mesh affects voice.
- Use headphones on mobile and Quest builds to prevent echo.
- Review microphone permissions in Electron or the Android system settings if the UI shows no input level.
- Keep LXST and Reticulum versions aligned with MeshChatX release notes when upgrading.
- **Docker / headless web**: containers have no PulseAudio host devices. MeshChatX forces the web audio bridge (MESHCHAT_FORCE_WEB_AUDIO=1) and installs hostless LXST backends so calls can use the browser mic/speaker. Enable telephone in settings, then place a call from the web UI over HTTPS.
- **Android Codec2**: native libcodec2.so must be preloaded before pycodec2. If Codec2 profiles are hidden, check /api/v1/telephone/codec2/status and rebuild with vendor wheels that bundle pycodec2/libcodec2.so.

## See also

- **LXMF messaging** for text conversations with the same peers
- **Identities, privacy, and security** for HTTPS and local access controls
- LXST project documentation for codec and session details

---

## Nomad Network and Mesh Server

Nomad Network is a distributed page and file system on top of Reticulum. MeshChatX includes a browser for remote nodes and a **Mesh Server** tool for hosting your own pages.

## Nomad browser

Open **Nomad Network** and enter a node destination hash. MeshChatX fetches the default entry page (usually /page/index.mu) over Reticulum link requests.

Supported page types:

| Extension | Format                               |
| --------- | ------------------------------------ |
| .mu     | Micron markup (NomadNet default)     |
| .md     | Markdown with GFM-oriented rendering |
| .txt    | Plain text with preserved whitespace |
| .html   | Static HTML with sanitised CSS       |

Follow links inside pages to browse further paths on the same node. Download files offered at /file/* paths.

Rendering uses NomadPageRenderer.js with DOMPurify sanitization. Micron can use a JavaScript parser or optional Go WASM when nomad_micron_wasm_enabled is set.

## Favourites and caching

Save frequent nodes as favourites. Link caching (nomadnet_cached_links) speeds up repeat visits on slow links.

## Archives

When **page archiver** is enabled, MeshChatX stores versioned snapshots of pages you visit. Open **Archives** to browse historical copies. An optional crawler can archive automatically.

Archived pages use the same renderer as the live browser based on the stored page_path extension.

**Private tabs** (incognito icon, purple strip) browse without writing archives, without favourites or Identify, and without reusing the shared Nomad link cache. Private tabs are not restored from localStorage. The destination hash stays out of the URL bar and browser history (same idea as SearXNG keeping queries off GET URLs). They use the same Reticulum process as the active identity. They do not create a separate IdentityContext.

## Mesh Server (page nodes)

**Tools -> Mesh Server** lets you run a nomadnetwork.node destination locally.

Typical workflow:

1. Create a page node in the UI.
2. Upload .mu, .md, .txt, or .html pages and optional files.
3. Start the node and announce it on the mesh.
4. Share your destination hash so others can open /page/index.mu on your node.

### Executable (dynamic) pages

You can opt in per node to **executable pages**. When enabled:

- Non-executable pages are served as static files.
- Pages marked executable in the Mesh Server editor run as scripts. On Linux and macOS, chmod +x on the page file also marks it.
- The first line must be a shebang such as #!/usr/bin/env python3. Windows does not exec scripts by shebang, so Mesh Server resolves that interpreter on PATH (python, py, node, and similar).
- Request field_* and var_* values are passed as environment variables.
- link_id and remote_identity are supplied when available.
- Script stdout is returned as the page body. Failures return a controlled error page.

Editing a page in Mesh Server always shows the file source, never the script output.

API endpoints under /api/v1/page-nodes/ manage CRUD operations, start and stop, and file listings.

Pages are served at /page/<name> and files at /file/<name> on the node destination.

## Browsing flow

```
User enters destination hash
    |
    v
RNS link request to /page/index.mu (or chosen path)
    |
    v
Remote page node responds with content
    |
    v
NomadPageRenderer picks Micron, Markdown, text, or HTML pipeline
    |
    v
Sanitised HTML shown in Nomad Network view
```

## Authoring pages

Read **NomadNet page formats** for security rules, Markdown quirks, and API behaviour. The Mesh Server rejects disallowed extensions on upload.

## Micron editor

**Tools -> Micron editor** helps author .mu pages before you upload them to your node.

## See also

- **NomadNet page formats** for detailed authoring reference
- **Tools and utilities** for the full tools list
- **Reticulum interfaces** if remote pages time out (likely a path issue)

---

## Reticulum interfaces

Interfaces connect your MeshChatX node to the Reticulum mesh. Manage them from the **Interfaces** page.

## What an interface does

Each interface is a Reticulum transport definition. Examples include TCP over the internet, UDP discovery, LoRa through an RNode, serial KISS devices, I2P tunnels, and automatic LAN discovery.

MeshChatX reads and writes interface configuration in your Reticulum config directory (default ~/.reticulum).

## Supported interface types

The **Add interface** flow includes:

| Type                  | Typical use                                   |
| --------------------- | --------------------------------------------- |
| TCPClientInterface    | Connect outbound to a known TCP peer          |
| TCPServerInterface    | Accept inbound TCP connections                |
| BackboneInterface     | High-throughput backbone link                 |
| UDPInterface          | UDP transport with discovery helpers          |
| RNodeInterface        | LoRa via RNode (serial, BLE, or IP transport) |
| RNodeIPInterface      | RNode reached over IP                         |
| SerialInterface       | Direct serial devices                         |
| KISSInterface         | KISS TNC devices                              |
| I2PInterface          | I2P-based Reticulum transport                 |
| AutoInterface         | Automatic discovery on local networks         |
| HTTPInterface         | HTTP/S tunnel (bundled RNS-over-HTTP)         |
| Custom external types | Advanced setups                               |

Community-curated suggestions come from bundled community_interfaces.json, built from [meshchatx.com/api/mcx-interfaces](https://meshchatx.com/api/mcx-interfaces) at release time. Browse listings at [meshchatx.com/interfaces](https://meshchatx.com/interfaces). An optional public/community_interfaces.json override can replace that list locally. The app does not fetch the directory over the network at runtime.

## Interface discovery

Discovery can automatically connect to peers on your LAN or configured networks. You can maintain allowlists and blocklists, set autoconnect behaviour, and assign a network identity for discovered peers.

## Import and export

Export your interface set for backup or clone it to another machine. Import validates entries before applying them.

## RNode tools

LoRa setups often need firmware management. **Tools -> RNode Flasher** opens the bundled flasher at /rnode-flasher/. Configure frequency, bandwidth, spreading factor, and TX power when adding an RNode interface.

## Websocket server interface

MeshChatX includes a custom WebsocketServerInterface for WebSocket-based Reticulum transport. Use it when bridging to web-friendly gateways.

## HTTP tunnel interface

MeshChatX vendors [RNS-over-HTTP](https://github.com/Quad4-Software/RNS-over-HTTP) and installs HTTPInterface.py into your Reticulum interfacepath on startup. Use **Add interface -> HTTP Tunnel** for client or server mode when only HTTP/S egress is available. Default transport is HTTP/1.1. HTTP/2 and HTTP/3 need TLS and optional extra packages on the server side.

## Getting onto the mesh

A minimal path for a new node:

```
Install MeshChatX
    |
    v
Add interface (TCP client, community suggestion, or RNode)
    |
    v
Reticulum establishes transport
    |
    v
Paths and announces populate in the UI
    |
    v
LXMF, LXST, and Nomad features become reachable
```

1. Pick a community interface or ask your mesh operator for TCP endpoint details.
2. Add the interface and enable it.
3. Watch the path table (**Tools -> RNPath**) if connectivity fails.
4. Enable **auto-announce** so your services are visible.

## I2P

I2P uses the local router's SAM API (usually 127.0.0.1:7656). Enable SAM in the router. Do not run Java I2P and i2pd at the same time.

MeshChatX allows one I2P interface, last in the list, with Transport Mode on. New interfaces default connectable off. Turn it on only if this node should accept inbound I2P peers. That makes the node an I2P transport. At least one b32.i2p peer is required.

## Bundled documentation hints

The Interfaces UI links into the Reticulum manual sections on interface options. Open **Documentation -> Reticulum** and search for interfaces if you need field-by-field reference.

## Tips

- Run only the interfaces you need. Each open port or radio adds attack surface and power draw.
- On Raspberry Pi and Android, prefer a single well-known TCP uplink if LoRa hardware is not attached.
- After editing Reticulum config externally, use the reload controls or restart MeshChatX so changes apply cleanly.
- Keep firmware on RNodes current using the flasher tool before debugging RF issues.

## See also

- **Installation and setup** for Reticulum config directory flags
- **Tools and utilities** for RNPath, RNProbe, and Ping
- Reticulum manual **Interfaces** chapter for protocol-level detail

---

## Tools and utilities

The **Tools** page groups mesh diagnostics and helper apps. Each tool opens its own view with a back link to the grid.

## Network diagnostics

| Tool               | Purpose                                            |
| ------------------ | -------------------------------------------------- |
| Ping               | Measure round-trip time to a reachable destination |
| RNProbe            | Probe whether a destination answers                |
| RNPath             | Inspect the path table                             |
| RNPath-trace       | Trace hops toward a destination                    |
| RNStatus           | Read node status information                       |
| Network visualiser | Graph view of topology (also in main navigation)   |

Use these when messages or pages fail despite interfaces showing as enabled.

## File transfer and shell

| Tool         | Purpose                                                  |
| ------------ | -------------------------------------------------------- |
| RNCP         | Send or fetch files over Reticulum                       |
| RNS FileSync | Sync a directory with peers over rns_filesync.filesync |
| RNSH         | Remote shell sessions with streamed output               |

RNCP progress events arrive on the WebSocket as rncp.transfer.progress.
FileSync progress and peer events use filesync.sync.progress, filesync.peer.connected, filesync.peer.disconnected, filesync.file.updated, filesync.file.deleted, and filesync.error.
FileSync uses the bundled rns_filesync package and keeps sync state under the active identity storage directory.

## Messaging helpers

| Tool              | Purpose                                         |
| ----------------- | ----------------------------------------------- |
| Propagation nodes | Manage LXMF propagation nodes and sync          |
| Forwarder         | Configure LXMF forwarding rules between aliases |
| Sieve filters     | Pattern-based inbound message filtering (beta)  |
| Message blocklist | Block known unwanted content (beta)             |
| Paper message     | Create or ingest LXMF URIs and QR workflows     |
| Bots              | Run subprocess LXMF bots from templates         |

Bot templates include echo, note, and reminder starters. They use the bundled lxmfy package.

## Content and publishing

| Tool          | Purpose                               |
| ------------- | ------------------------------------- |
| Mesh Server   | Host NomadNet-compatible page nodes   |
| Micron editor | Edit .mu pages locally              |
| Documentation | MeshChatX guides and Reticulum manual |

## Configuration editors

| Tool                    | Purpose                                 |
| ----------------------- | --------------------------------------- |
| Reticulum config editor | Edit raw Reticulum configuration        |
| Repository server       | Host Python wheels for offline installs |

## Hardware and translation

| Tool          | Purpose                                              |
| ------------- | ---------------------------------------------------- |
| RNode flasher | Flash or update RNode firmware                       |
| Translator    | Translate text via Argos Translate or LibreTranslate |

Translator calls respect **privacy mode**. When privacy mode blocks outbound HTTP, external translation endpoints are not contacted.

## Debugging

| Tool       | Purpose                       |
| ---------- | ----------------------------- |
| Debug logs | View backend debug log stream |

## Coming soon

The registry marks **RNS Tunnel** as coming soon. It does not have a route in the current release.

## Relay chat server

When rrc_enabled is on, you can run a local RRC hub from relay chat server settings. Hubs announce aspect rrc.hub. Client UI lives under **Relay chat** in the main navigation.

## Plugins

Installed plugins can add rows to **Tools** and **Navigation** through contribution manifests. Example bundled plugin: **Bug Reports** (com.meshchatx.mcx-bugs) for sending redacted debug logs to an mcx-bugs-v1 collector (or running a collector yourself).

Plugins are capability-gated, not fully open-ended: they cannot rewrite core MeshChatX. Supported packaged runtimes are **frontend JS** (Worker), optional **backend WASM** (wasmtime), and optional **backend Python** (backend.type: "python"). Install sources include ZIP archives and single-file **WASM bundles** with embedded plugin.json / files / optional RSG signature.

ZIP and WASM installs show a confirmation dialog with requested permissions, scanned/declared external HTTP URLs, signature status (unsigned / signed / trusted / invalid), and heuristic security findings. Invalid signatures hard-block install. You can deny individual grants and optionally trust a valid signer. After install, MeshChatX stores an integrity hash and auto-disables tampered plugins.

Optional **Sideband-compatible** plugins load flat *.py files from a configured directory when the master switch is enabled (danger confirm). They run in-process with full host access. Optional sibling filename.py.rsg signatures are verified over file bytes.

Sign packages with scripts/sign-plugin.py (sign|verify for -dir|-zip|-wasm|-py) using a Reticulum identity (rnid or in-process RNS.Identity). MeshChatX plugin signing uses signature file meshchatx.plugin.rsg and WASM custom sections meshchatx.plugin / meshchatx.files / meshchatx.signature.

Disable packaged plugins at startup with --disable-plugins if you need a minimal surface.

## Command palette

Press the command palette shortcut (configured in settings) to jump to tools and pages without returning to the grid.

## Choosing a tool

```
Symptom                          Tool to try first
-------------------------------- -----------------
No peers visible                 Interfaces, then RNPath
Message stuck sending            RNPath, Propagation nodes
Cannot reach Nomad page          Ping, RNProbe
Need to push a file              RNCP
Remote administration            RNSH (with care)
Want offline Python packages     Repository server
```

## See also

- **Reticulum interfaces** for transport setup
- **LXMF messaging** and **Nomad Network** for feature-specific workflows
- [Plugins](plugins) for extending MeshChatX functionality
- **Documentation** for offline manuals

---

## Identities, privacy, and security

MeshChatX separates cryptographic identities, network security, and optional privacy controls. This page summarises how they interact.

## Identities

Each identity is a Reticulum key pair with its own:

- SQLite database and LXMF router directory
- Settings in the config table via ConfigManager
- Storage path under storage/identities/<identity_hash>/

Create, import, or switch identities from **Identities**. Only one identity is active in the UI at a time. Switching runs a teardown path so routers and managers do not leak state.

Shared resources include the Reticulum process and interface configuration in ~/.reticulum unless you override paths.

## Announces

MeshChatX tracks announces for aspects such as:

| Aspect              | Meaning                           |
| ------------------- | --------------------------------- |
| lxmf.delivery     | Peer accepts LXMF messages        |
| lxst.telephony    | Peer accepts LXST calls           |
| lxmf.propagation  | Propagation node                  |
| nomadnetwork.node | NomadNet page server              |
| rrc.hub           | Relay chat hub (when RRC enabled) |
| map-data-v1       | Published GeoJSON/KML/KMZ packs   |

Announce records store signal metadata and parsed app data for display names and icons.

## Web UI authentication

Optional HTTP basic authentication is enabled with --auth or MESHCHAT_AUTH=true. Sessions use encrypted cookies. Mutating API requests require CSRF tokens.

Access attempts are logged. Repeated failures can trigger lockout when auth is enabled.

Reset a forgotten password with --reset-password or MESHCHAT_RESET_PASSWORD=true, then set a new password in the UI.

### Demo mode and ALTCHA

MESHCHAT_DEMO_MODE=1 (or --demo) enables a public showcase profile: privacy mode on, plugins off, no outbound announces, and a default-deny HTTP mutation policy with mesh send blocked. Status reports demo_mode: true.

When MESHCHAT_ALTCHA_ENABLED=1, login and setup require a valid [ALTCHA](https://altcha.org/docs/v2/widget-v3/) proof-of-work payload (widget v3, server challenges use PBKDF2/SHA-256 by default). Set MESHCHAT_ALTCHA_HMAC_KEY to a long random secret on the server. Optional MESHCHAT_ALTCHA_COST tunes PoW difficulty. The widget loads from the bundled altcha npm package and fetches challenges from /api/v1/auth/altcha/challenge.

MESHCHAT_AUTH_PAGE_HINT sets optional plain text on the login page (independent of demo mode). Demo Docker compose defaults to username and password hints for the showcase account.

MESHCHAT_AUTH_BYPASS=1 skips session auth for local testing only. Do not use it on internet-facing deployments.

## Transport security

- HTTPS and WSS are on by default.
- Self-signed certificates are generated per identity when custom PEM files are missing.
- Pass --ssl-cert and --ssl-key for managed certificates.
- Use --no-https only on trusted loopback setups.
- WebSocket upgrades require a same-authority Origin when the browser sends one. Missing Origin is allowed on loopback binds (and when password auth is enabled) so local non-browser tools keep working. On a non-loopback bind without password auth, a missing Origin is rejected.
- /ws inbound messages are rate-limited per connection. Nomad file downloads over WS are capped (default 10 MiB) and large transfers use chunked frames. Debug counters are at GET /api/v1/debug/websocket.

Electron loads the UI from the local HTTPS origin served by the embedded backend.

## IP allowlisting

app_security_settings can restrict which client IPs may use the web UI. Combine with auth when exposing the service beyond localhost.

## Privacy mode

**Privacy mode** blocks outbound HTTP from MeshChatX features that would otherwise call the public internet. Translation and similar tools respect this flag.

Privacy mode does not disable Reticulum mesh traffic. It limits clearnet fetches from the app itself.

## Linux sandboxing

On Linux, MeshChatX can enable two complementary in-process sandboxes when supported:

- **Landlock** restricts filesystem paths the backend may use. User-local pipx tools (for example Argos Translate under ~/.local) need explicit read and sometimes write roots. See **Linux sandboxing** in Platform guides.
- **Seccomp-BPF** installs a syscall denylist (via libseccomp) that blocks kernel-admin and related calls a mesh client does not need.

Both auto-enable when available and fall back to a no-op when the platform, kernel, or libraries cannot support them. Override with:

- MESHCHAT_LANDLOCK=0 or 1
- MESHCHAT_SECCOMP=0 or 1

Android never enables these in-process sandboxes (the Android app seccomp policy already constrains the process, and Landlock syscalls are blocked there).

See **Linux sandboxing** in Platform guides for optional Firejail and Bubblewrap wrappers around the host install.

## Windows Electron AppContainer

Windows desktop builds can spawn the Python backend inside an LPAC AppContainer when MESHCHAT_APPCONTAINER=1. Default installs start the backend directly without that wrapper. Check /api/v1/server/security for appcontainer_active when debugging sandbox-related SQLite or filesystem errors on Windows.

## Blocking and filtering

Use **Blocked** for specific destination hashes. Combine with sieve filters, message blocklists, and LXMF stamp policies described in **LXMF messaging**.

## Data backup

Database backups land in database-backups/. Before a schema upgrade, MeshChatX writes a backup-pre-migrate-v*-to-v*.zip in that folder unless MESHCHAT_SKIP_PRE_MIGRATE_BACKUP=1. After a successful migration it runs PRAGMA quick_check and keeps the five newest pre-migrate zips (override with MESHCHAT_PRE_MIGRATE_BACKUP_KEEP, 0 disables pruning). If the stored schema version is newer than this build supports, startup refuses to migrate. Only one process should use a given identity storage directory at a time (storage lock). Roll back by restoring a backup zip and running an older MeshChatX build. Export snapshots from **About** or the API. Electron crash recovery can offer restore when integrity checks fail.

CLI examples:

```bash
meshchatx --list-backups
meshchatx --export-backup /path/to/export.zip
meshchatx --export-backup backup-20260101-120000.zip /path/to/copy.zip
meshchatx --restore-db /path/to/backup.zip
```

When the backend can start briefly, or you run from source:

```bash
meshchatx --storage-dir /path/to/storage --restore-db /path/to/backup.zip
```

## Database corruption and data reset

If MeshChatX fails to start with errors such as database disk image is malformed, DatabaseError, or corrupted ratchet data, the desktop crash screen offers:

- Restore latest backup from database-backups/ or snapshots/ inside the MeshChatX storage folder
- Choose backup file for a zip you saved elsewhere
- Try auto-repair (--auto-recover: SQLite checkpoint / integrity pass)
- Emergency mode, which opens the app without the database so you can export from About when possible
- Copy reset instructions with the folders to delete for a clean reinstall

### Storage locations

| Platform         | MeshChatX storage                              | Reticulum network stack              |
| ---------------- | ---------------------------------------------- | ------------------------------------ |
| Linux / macOS    | ~/.reticulum-meshchatx/                      | ~/.reticulum/                      |
| Windows          | %USERPROFILE%\.reticulum-meshchatx\          | %USERPROFILE%\.reticulum\          |
| Windows portable | <MeshChatX.exe folder>\.reticulum-meshchatx\ | <MeshChatX.exe folder>\.reticulum\ |

Legacy Reticulum MeshChat data may still exist at ~/.reticulum-meshchat/ (or the Windows equivalent). Automatic database backups go to database-backups/ inside the MeshChatX storage folder after a successful run.

### Complete removal

Quit MeshChatX. On Windows, also end ReticulumMeshChatX.exe in Task Manager if it is still running. Then delete the MeshChatX storage folder and the Reticulum config folder for your install type. That removes the local identity, messages, contacts, path cache, and ratchet state. The next launch creates a new identity unless you restore a backup first.

Linux / macOS:

```bash
rm -rf ~/.reticulum-meshchatx ~/.reticulum ~/.reticulum-meshchat
```

Windows PowerShell:

```powershell
Remove-Item -Recurse -Force "$env:USERPROFILE\.reticulum-meshchatx", "$env:USERPROFILE\.reticulum", "$env:USERPROFILE\.reticulum-meshchat" -ErrorAction SilentlyContinue
```

If you pass --storage-dir or --reticulum-config-dir, delete those directories instead.

## Integrity checks

Startup integrity verification runs in packaged Electron builds and can be triggered from the backend. Failed checks surface recovery options instead of silently corrupting data.

## Plugin signing and trust

Packaged plugins may include a Reticulum Signature (.rsg) over a canonical ZIP payload (sorted paths, fixed 1980-01-01 mtimes, signature file excluded). MeshChatX plugin signing writes meshchatx.plugin.rsg and WASM sections meshchatx.plugin / meshchatx.files / meshchatx.signature.

Policy:

- Unsigned packages are allowed
- Present but invalid signatures hard-block install
- Valid signers can be added to a user trusted-publishers list (ignored if the list file is tampered outside MeshChatX)
- Installed plugin trees get an integrity hash, on-disk changes disable the plugin as tampered

Sideband Python plugins are opt-in via a master danger switch. They are not ZIP-permission gated. Optional per-file .py.rsg signatures are verified over script bytes.

## Safe deployment patterns

```
Recommended for most users
    |
    v
Bind 127.0.0.1, use HTTPS, enable auth if others use the same host
    |
    v
Add interfaces only for meshes you trust
    |
    v
Keep backups and test restore on upgrades
```

Avoid exposing port 8000 directly to the internet without a reverse proxy, strong auth, and network-level filtering. MeshChatX is designed as a personal or small-team operator console, not a multi-tenant public website.

## Multi-user hosts

On shared computers, use separate OS user accounts or separate --storage-dir values so SQLite databases and identity files do not overlap.

## See also

- **Architecture and design** for session and API details
- **Installation and setup** for CLI security flags
- Reticulum manual cryptography chapters for identity math

---

## Plugins

Plugins extend MeshChatX with extra tools, nav items, and background behaviour. They are capability-gated: a plugin only gets what you grant at install time.

Manage them from **Settings -> Plugins**. Disable every packaged plugin at startup with --disable-plugins or MESHCHAT_DISABLE_PLUGINS=true.

## What plugins can do

- Add a row on the **Tools** page
- Add an item in the main **Navigation** sidebar
- React to mesh events (announces, RNS link traffic)
- Call narrowly declared backend managers (path table, debug log, bug reports, RNS links)
- Keep a private key-value store (storage: isolated)
- Optionally fetch clearnet HTTP (network: fetch), still subject to **Privacy mode**

Plugins cannot rewrite core MeshChatX. They do not get open-ended filesystem or process control unless you opt into Sideband Python plugins (see below).

## Runtimes

| Runtime         | Where it runs             | Trust level                                     |
| --------------- | ------------------------- | ----------------------------------------------- |
| Frontend JS     | Browser Web Worker        | Medium. Sandboxed worker, capability grants     |
| Backend WASM    | wasmtime on the server  | Medium. Fuel-metered, capability-gated host     |
| Backend Python  | In-process with MeshChatX | High. Permission-checked in-process host        |
| Sideband *.py | In-process, flat files    | Highest. Opt-in danger switch, full host access |

A packaged plugin can ship frontend only, backend only, or both.

## Install flow

```
Pick ZIP or .wasm file in Settings -> Plugins
    |
    --> Preview (permissions, URLs, signature, findings)
    |
    --> You grant or deny each capability
    |
    --> Optional: trust a valid signer
    |
    --> Install + integrity hash stored
    |
    --> Enable
    |
    --> Frontend Worker loads (if present)
    --> Backend WASM / Python activates (if present)
```

Invalid signatures hard-block install. Unsigned packages are allowed. Present-but-broken signatures are not.

After install, MeshChatX hashes the on-disk tree. If files change outside the app, the plugin is auto-disabled as tampered.

## Bundled example: Bug Reports

com.meshchatx.mcx-bugs ships with MeshChatX. It adds a **Bug Reports** tool for sending redacted debug logs to an mcx-bugs-v1 collector, or running a collector yourself.

Layout:

```
mcx-bugs/
    plugin.json
    frontend/main.js
    backend/main.py
    locales/en.json
```

Use it as the reference package when building your own.

## Manifest (plugin.json)

Every packaged plugin needs a root plugin.json.

```json
{
    "id": "com.example.my-plugin",
    "version": "1.0.0",
    "apiVersion": 1,
    "name": "My Plugin",
    "description": "Adds a custom tool.",
    "frontend": {
        "entry": "frontend/main.js",
        "type": "js"
    },
    "backend": {
        "entry": "backend/main.py",
        "type": "python"
    },
    "i18n": {
        "directory": "locales",
        "defaultLocale": "en"
    },
    "contributes": {
        "navItems": [
            {
                "id": "my-plugin",
                "route": { "name": "plugin-my-plugin" },
                "icon": "puzzle",
                "labelKey": "nav"
            }
        ],
        "toolsPageEntries": [
            {
                "name": "my-plugin",
                "route": { "name": "plugin-my-plugin" },
                "icon": "puzzle",
                "titleKey": "title",
                "descriptionKey": "description"
            }
        ]
    },
    "permissions": {
        "hooks": ["announce.received"],
        "managers": ["destinationPath.read"],
        "storage": "isolated",
        "network": "none"
    }
}
```

Notes:

- id is reverse-DNS style and must stay stable across versions
- apiVersion is currently 1
- Plugin strings live in the plugin bundle (locales/{locale}.json), not core en.json
- contributes wires UI slots through the frontend registries

## Permissions

Nothing is available unless it is declared in the manifest and granted in the install dialog.

### Hooks

| Hook                | When it fires                                               |
| ------------------- | ----------------------------------------------------------- |
| announce.received | A Reticulum announce arrives                                |
| rns.link.event    | Generic RNS Link traffic (packet_received, link_closed) |

Hook events reach the UI as WebSocket plugin.event frames, then into the plugin Worker.

### Managers

| Manager                | Purpose                       |
| ---------------------- | ----------------------------- |
| destinationPath.read | Read the Reticulum path table |
| debugLog.read        | Read redacted debug logs      |
| bugReport.*          | Bug report / collector APIs   |
| rnsLink.open         | Open or reuse an RNS link     |
| rnsLink.identify     | Identify on a cached link     |
| rnsLink.request      | Request/response on a link    |
| rnsLink.send         | Send a raw link packet        |
| rnsLink.close        | Tear down a cached link       |

Call managers from a plugin with POST /api/v1/plugins/{id}/invoke and method: "callManager". Details for the link transport are in [RNS Link API](rns-link-api).

### Storage and network

| Permission          | Effect                                                                |
| ------------------- | --------------------------------------------------------------------- |
| storage: isolated | Private key-value store in the MeshChatX database                     |
| storage: none     | No plugin storage                                                     |
| network: fetch    | Outbound HTTP allowed (still blocked by Privacy mode when that is on) |
| network: none     | No clearnet fetch                                                     |

Install preview also scans plugin files for external http:// / https:// URLs and shows them before you grant network access.

## How a frontend plugin runs

```
Settings enable plugin
    |
    --> PluginHost loads /api/v1/plugins
    |
    --> Fetch frontend entry as text
    |
    --> Spawn pluginWorker.js (module Worker)
    |
    --> Register nav / tools contributions
    |
    --> Subscribe to plugin.event on /ws (if hooks granted)
    |
    --> Worker may invoke backend via /api/v1/plugins/{id}/invoke
```

The Worker talks to the host with typed messages (init, event, request). The host never gives the Worker a raw privileged API.

## How a backend plugin runs

```
Enable plugin
    |
    +--> type: wasm  --> load into wasmtime, fuel + host caps
    |
    +--> type: python --> import entry, call activate(host)
    |
    --> Hooks fan out from PluginManager
    |
    --> invoke(method, args) for RPC from the UI Worker
```

Python host surface (permission-checked):

- host.log(message)
- host.call_manager(capability, args)
- host.storage_get(key) / host.storage_set(key, value)
- host.network_fetch_allowed()

## Packaging and signing

Distribute as:

1. **ZIP** with plugin.json and assets
2. **WASM bundle** (single .wasm with embedded manifest / files / optional signature)

Signature file for ZIP/dir packages: meshchatx.plugin.rsg

WASM custom sections:

```
meshchatx.plugin      --> embedded plugin.json
meshchatx.files       --> embedded text assets
meshchatx.signature   --> RSG over payload without this section
```

Canonical ZIP signing uses sorted paths and fixed 1980-01-01 mtimes. The signature file itself is excluded from the signed payload.

Sign and verify with:

```bash
python3 scripts/sign-plugin.py sign-dir ./my-plugin --identity <rnid>
python3 scripts/sign-plugin.py verify-dir ./my-plugin
python3 scripts/sign-plugin.py sign-zip ./my-plugin.zip --identity <rnid>
python3 scripts/sign-plugin.py sign-wasm ./plugin.wasm --identity <rnid>
python3 scripts/sign-plugin.py sign-py ./legacy_plugin.py --identity <rnid>
```

Trust status in the UI:

```
No .rsg present
    --> Unsigned (install allowed)

Valid .rsg, signer unknown
    --> Signed (you can add to Trusted Publishers)

Valid .rsg, signer in Trusted Publishers
    --> Trusted

Broken / mismatched .rsg
    --> Invalid (install blocked)
```

## Sideband-compatible plugins

Legacy Sideband-style flat *.py files are separate from packaged ZIP/WASM plugins.

```
Settings -> Plugins -> Sideband
    |
    --> Confirm danger prompt
    |
    --> Set directory of *.py files
    |
    --> Optional filename.py.rsg next to each script
    |
    --> Reload
```

These run in-process with full host access. They are not ZIP-permission gated. Keep the master switch off unless you trust every file in that directory.

## Operator tips

- Prefer signed packages from publishers you added yourself
- Deny network: fetch unless the plugin needs clearnet HTTP
- Prefer WASM backends over Python when you can
- Use --disable-plugins when diagnosing weird UI or backend behaviour
- Treat Sideband plugins like running arbitrary local scripts

## See also

- [Tools and utilities](tools) for the Tools page and contribution overview
- [RNS Link API](rns-link-api) for rnsLink.* and rns.link.event
- [Architecture and design](architecture) for the plugin runtime overview
- [Identities, privacy, and security](identity-and-security) for signing and Privacy mode

---

## RNS Link API

MeshChatX exposes a generic Reticulum Link transport on the main WebSocket (/ws). External apps and plugins can open links, run request/response exchanges, send packets, and tear links down without going through NomadNet helpers.

## When to use it

```
Your app or plugin needs a live RNS Link
    |
    --> Not NomadNet page browsing
    --> Not LXMF messaging
    |
    --> Use rns.link.* over /ws
        or plugin managers rnsLink.*
```

Address peers by destination hash and aspect. Do not invent IP or hostname shortcuts.

## Auth

When password auth is enabled, every rns.link.* client message needs an authenticated session. Same rule as other WebSocket mutators.

## Link lifecycle

```
Client sends rns.link.open
    |
    --> MeshChatX finds or opens path to destination
    |
    --> Link cached under (aspect, destination_hash)
    |
    --> Optional auto_identify
    |
    --> success / failure reply on same type + request_id
    |
    +--> rns.link.request / rns.link.send on the cached link
    |
    +--> rns.link.close tears down and uncaches
    |
    +--> disconnect cancels in-flight open / request for that client
```

Cache notes:

- Key is (aspect, destination_hash)
- Cap is 64 active links
- Idle links expire after about 30 minutes
- Repeated request failures recycle the cached link so the next call re-opens

## Client to server

All messages need a unique request_id so replies can be matched.

| type              | Required fields                                           | Optional              | Behaviour                                                                |
| ------------------- | --------------------------------------------------------- | --------------------- | ------------------------------------------------------------------------ |
| rns.link.open     | destination_hash, aspect, request_id                | auto_identify       | Open or reuse a cached link. Streams phase then success / failure. |
| rns.link.identify | destination_hash, aspect, request_id                |                       | Call link.identify(local_identity) on the cached link.                 |
| rns.link.request  | destination_hash, aspect, path, request_id        | data_b64, timeout | Ensure the link is open, then link.request(path, data=…).              |
| rns.link.send     | destination_hash, aspect, payload_b64, request_id |                       | Send a raw packet on the cached link.                                    |
| rns.link.close    | destination_hash, aspect, request_id                |                       | Teardown and uncache the link.                                           |

Field details:

- destination_hash: hex string of the peer destination
- aspect: dot-separated RNS app name + sub-aspects, for example microrn.mgmt
- data_b64 / payload_b64 / reply body_b64: msgpack payloads, base64-encoded (size-capped on the server)
- path: request path string on the remote link endpoint
- timeout: seconds for the request wait

Optional binary frames: send { "type": "ws.caps", "binary_rns_link": true } first. After that, binary WebSocket frames carrying msgpack dicts with the same fields as the JSON messages are accepted. JSON remains the default and is always supported.

Example open:

```json
{
    "type": "rns.link.open",
    "destination_hash": "aabbccddeeff00112233445566778899aabbccdd",
    "aspect": "microrn.mgmt",
    "request_id": "req-1",
    "auto_identify": true
}
```

Example request:

```json
{
    "type": "rns.link.request",
    "destination_hash": "aabbccddeeff00112233445566778899aabbccdd",
    "aspect": "microrn.mgmt",
    "path": "/status",
    "request_id": "req-2",
    "data_b64": null,
    "timeout": 15
}
```

## Server to client

Per-request_id replies reuse the same type with a status:

| status   | Meaning                                      |
| ---------- | -------------------------------------------- |
| phase    | Progress step while opening or requesting    |
| progress | Additional progress detail when available    |
| success  | Operation finished                           |
| failure  | Operation failed (includes an error message) |

Broadcast events (not tied to one request_id):

| type           | event           | Notes                  |
| ---------------- | ----------------- | ---------------------- |
| rns.link.event | packet_received | Includes payload_b64 |
| rns.link.event | link_closed     | Cached link removed    |

```
Inbound packet on a cached link
    |
    --> Broadcast rns.link.event / packet_received
    |
Link torn down or evicted
    |
    --> Broadcast rns.link.event / link_closed
```

## Plugins

Plugins call the same transport through HTTP invoke instead of speaking WebSocket types directly.

```
Plugin Worker
    |
    --> POST /api/v1/plugins/{id}/invoke
        method: "callManager"
    |
    --> PluginManager checks granted managers
    |
    --> RnsLinkManager open / identify / request / send / close
```

Declare managers in plugin.json:

| Manager            | Maps to                 |
| ------------------ | ----------------------- |
| rnsLink.open     | Open or reuse link      |
| rnsLink.identify | Identify on cached link |
| rnsLink.request  | Request/response        |
| rnsLink.send     | Raw packet send         |
| rnsLink.close    | Teardown                |

Subscribe to async traffic with:

```json
{
    "permissions": {
        "hooks": ["rns.link.event"],
        "managers": ["rnsLink.open", "rnsLink.identify", "rnsLink.request", "rnsLink.send", "rnsLink.close"],
        "storage": "isolated",
        "network": "none"
    }
}
```

Hook delivery:

```
RnsLinkManager event
    |
    --> PluginManager.dispatch_hook("rns.link.event", …)
    |
    --> WebSocket plugin.event to the UI
    |
    --> Plugin Worker on_hook / event handler
```

## External app pattern

```
Connect to MeshChatX /ws (auth cookie / session as required)
    |
    --> Send rns.link.open with request_id
    |
    --> Wait for matching success
    |
    --> Send rns.link.request or rns.link.send
    |
    --> Listen for rns.link.event broadcasts
    |
    --> Send rns.link.close when finished
```

Keep one request_id per outstanding call. Cancel or ignore replies after you disconnect. MeshChatX cancels in-flight open/request work for that WebSocket client on disconnect.

## Limits and failure behaviour

- Missing path or unreachable peer returns failure on the open/request reply
- After repeated request failures on one cached link, MeshChatX recycles that link
- Idle unused links are swept after about 30 minutes
- Over-cap eviction drops the oldest unused links first

## Implementation map

```
/ws rns.link.*
    |
    --> meshchat.py WebSocket dispatch + per-client task tracking
    |
    --> rns_link_manager.py cache, open, identify, request, send, close
    |
    --> plugin_manager.py capability wrappers + hook fan-out
```

## See also

- [Plugins](plugins) for install, grants, and invoke flow
- [Architecture and design](architecture) for WebSocket and plugin runtime overview
- [Identities, privacy, and security](identity-and-security) for auth and session rules

---

## NomadNet page formats

MeshChatX serves pages from a **Mesh Server** page node and displays them in the **Nomad Network** browser. Pages are fetched with the Nomad path convention /page/<filename>.

## Supported filenames

| Extension | Role                                                    |
| --------- | ------------------------------------------------------- |
| .mu     | Micron markup (NomadNet default)                        |
| .md     | Markdown with GitHub-flavored features via the renderer |
| .txt    | Plain text with escaped HTML and preserved whitespace   |
| .html   | Static HTML with CSS only (see security below)          |

If you add a page without a recognised extension, the server stores it as .mu. Filenames with other extensions (for example .exe) are rejected when saving through the API.

## Plain text (.txt)

Content is HTML-escaped and shown with pre-wrapped whitespace. There is no Markdown parsing on .txt pages.

## Markdown (.md)

**Not the same engine as chat.** Conversations use the lightweight MarkdownRenderer in the messaging UI. Nomad .md pages use marked with GFM-oriented rules plus sanitisation. Features and edge cases can differ between the two paths. Automated tests cover both.

Authoring tips:

- Use ATX headings with a hash and a space before the title, for example # Title, ## Section, #### Subsection.
- Fenced code blocks keep indentation.
- Off-mesh http and https links in rendered content are removed or restricted so the preview cannot drive external navigation without mesh-style URLs.

## HTML (.html)

- **JavaScript** is not executed. script tags and event-handler attributes are stripped.
- **External resources** are blocked where possible. @import and url(...) pointing at http://, https://, or protocol-relative URLs are removed from CSS.
- Embedded <style> blocks are kept. Rules that target html or body are rewritten to apply to the viewer root container.
- **Links** must be mesh-style (: paths, 32-character hex prefixes, /page/..., /file/..., or # fragments) or they are removed.
- **Images** only keep data:image/... inline sources.
- The viewer uses a sans-serif font for HTML and Markdown so pages do not inherit Micron monospace chrome. Override colours and typography with your own CSS.

## Mesh Server API

- POST /api/v1/page-nodes/{node_id}/pages with name and content saves a page. Invalid extensions return HTTP 400 with a short message.
- Optional executable marks the page as a shebang script. The node must also have executable pages enabled, or the file is served as static text.
- Listed pages only include files with allowed extensions in the pages/ directory.

## Archives

Snapshots in **Archives** use the same rendering pipeline as the Nomad browser. The archived page_path extension selects Micron, Markdown, text, or HTML handling. Exports keep the original extension when it is .mu, .md, .txt, or .html.

## See also

- **Nomad Network and Mesh Server** for browsing and hosting workflows
- **Architecture and design** for where page nodes fit in the backend
- Default Nomad entry path remains /page/index.mu unless you change the URL in the browser

---

## Raspberry Pi

This guide shows a simple headless setup for running MeshChatX on a Raspberry Pi 4
with a web UI you can access from another device on your network.

This install path uses a release wheel, which already includes frontend assets.

## Automated Setup Scripts

```bash
curl -fsSL 'https://raw.githubusercontent.com/Quad4-Software/MeshChatX/refs/heads/master/scripts/rpi/install_meshchatx.sh' | bash
```

If you have the repo cloned locally already:

```bash
bash scripts/rpi/install_meshchatx.sh
```

The installer guides you through:

- Optional espeak-ng install (tries apt/dnf/pacman)
- Install method (pipx or venv + pip)
- Wheel choice (latest stable, latest pre-release, or a custom URL)
- Optional **cosign** attestation: if a *.whl.cosign.bundle is published
  next to the wheel, you can verify it. The script uses cosign on PATH if
  present, or downloads a **checksum-verified** official Linux binary to /tmp
  (the Sigstore bundle format is not reimplemented in shell, so you still use the
  real cosign to verify, without installing a distro package)
- Storage and Reticulum directories
- Bind host and port (with availability check)
- HTTPS on/off (default on)
- Service mode (system, user, or none)
- Service startup validation via the HTTP status endpoint

If startup validation fails, it prints recent logs and stops the service to avoid
restart loops.

## 1) Install Base Dependencies

```bash
sudo apt update
sudo apt upgrade -y
sudo apt install -y python3 python3-pip pipx
```

## 2) Enable pipx Path

```bash
pipx ensurepath
source ~/.profile
```

If pipx is not available in your distro package repo, install it with:

```bash
python3 -m pip install --user pipx
python3 -m pipx ensurepath
source ~/.profile
```

## 3) Install MeshChatX with pipx (recommended)

Preferred option (recommended): install from a release wheel (4.8.6 or newer),
because the wheel bundles frontend assets.

```bash
pipx install /path/to/reticulum_meshchatx-<version>-py3-none-any.whl
```

Direct example (v4.8.6):

```bash
pipx install "https://github.com/Quad4-Software/MeshChatX/releases/download/v4.8.6/reticulum_meshchatx-4.8.6-py3-none-any.whl"
```

py3-none-any wheels are architecture-independent, so the same wheel artifact
works on Raspberry Pi ARM and x86_64 Linux systems.

Upgrade example:

```bash
pipx upgrade meshchatx
```

## 4) Install MeshChatX without pipx (venv + pip)

If you prefer not to use pipx:

```bash
mkdir -p ~/meshchatx
cd ~/meshchatx
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install "https://github.com/Quad4-Software/MeshChatX/releases/download/v4.8.6/reticulum_meshchatx-4.8.6-py3-none-any.whl"
```

Run command in venv mode:

```bash
~/meshchatx/.venv/bin/meshchatx --headless --host 0.0.0.0 --port 8000
```

## 5) Run MeshChatX (Headless)

```bash
meshchatx --headless --host 0.0.0.0 --port 8000
```

Then open:

```bash
http://<pi-ip>:8000
```

## 6) Configure a systemd Service

systemd keeps MeshChatX running in the background and starts it automatically
on boot.

You have two service styles:

- System service (/etc/systemd/system/...) for always-on host services.
- User service (~/.config/systemd/user/...) for per-user sessions.

### Option A: System service (recommended for Pi node/server use)

Create /etc/systemd/system/meshchatx.service:

```ini
[Unit]
Description=MeshChatX Headless (system service)
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=pi
Group=pi
WorkingDirectory=/home/pi/meshchatx
Environment="PATH=/home/pi/.local/bin:/usr/bin:/bin"
ExecStart=/home/pi/.local/bin/meshchatx --headless --host 0.0.0.0 --port 8000 --storage-dir /home/pi/meshchatx/storage --reticulum-config-dir /home/pi/.reticulum
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target
```

The above service file is for pipx installs. For venv installs, use:

```ini
[Service]
Type=simple
User=pi
Group=pi
WorkingDirectory=/home/pi/meshchatx
Environment="PATH=/home/pi/meshchatx/.venv/bin:/usr/bin:/bin"
ExecStart=/home/pi/meshchatx/.venv/bin/meshchatx --headless --host 0.0.0.0 --port 8000 --storage-dir /home/pi/meshchatx/storage --reticulum-config-dir /home/pi/.reticulum
Restart=always
RestartSec=3
```

Update User, Group, and paths if your install location is different.

Enable and start:

```bash
mkdir -p /home/pi/meshchatx/storage /home/pi/.reticulum
sudo chown -R pi:pi /home/pi/meshchatx
sudo systemctl daemon-reload
sudo systemctl enable --now meshchatx.service
sudo systemctl status meshchatx.service
```

### Option B: User service (no sudo system unit)

Create ~/.config/systemd/user/meshchatx.service:

```ini
[Unit]
Description=MeshChatX Headless (user service)
After=network-online.target

[Service]
Type=simple
WorkingDirectory=%h/meshchatx
Environment="PATH=%h/.local/bin:/usr/bin:/bin"
ExecStart=%h/.local/bin/meshchatx --headless --host 0.0.0.0 --port 8000 --storage-dir %h/meshchatx/storage --reticulum-config-dir %h/.reticulum
Restart=always
RestartSec=3

[Install]
WantedBy=default.target
```

Enable/start user service:

```bash
systemctl --user daemon-reload
systemctl --user enable --now meshchatx.service
systemctl --user status meshchatx.service
```

If you want user services to stay active without login:

```bash
sudo loginctl enable-linger pi
```

### Service management commands

```bash
sudo systemctl restart meshchatx.service
sudo systemctl stop meshchatx.service
sudo systemctl disable meshchatx.service
```

Useful logs and troubleshooting:

```bash
journalctl -u meshchatx.service -f
journalctl -u meshchatx.service -n 200 --no-pager
systemctl show meshchatx.service -p ExecStart -p User -p Group
```

## Reset Password

If you forget the web UI password and have SSH access to the Pi, reset it with the --reset-password flag:

```bash
meshchatx --reset-password --headless --host 0.0.0.0 --port 8000
```

Or set the environment variable:

```bash
MESHCHAT_RESET_PASSWORD=true meshchatx --headless --host 0.0.0.0 --port 8000
```

This clears the stored password hash on startup. Open the web UI and you will see the Initial Setup screen where you can set a new password. After resetting, you can stop the app and restart without the flag.

## Notes

- Reticulum configuration and identity data are stored in the service user's home
  directory by default (for example ~/.reticulum and MeshChatX storage paths).
- If you attach RNode hardware by USB, make sure the service user has permission
  to access serial devices (dialout group on Debian-based systems).

---

## Android (Termux)

MeshChatX runs on Android through [Termux](https://termux.dev/). Release wheels ship the Python backend and built web UI, so you do not need Node on the phone for a normal install.

## Install from wheel

The wheel bundles server code and frontend assets.

### System packages

```
pkg upgrade
pkg install python
pkg install rust
pkg install binutils
pkg install build-essential
```

> Note: Python 3.11 or higher is required. Check with python --version.

### Wheel install

Download the latest wheel from the [releases page](https://github.com/Quad4-Software/MeshChatX/releases), then:

```
pip install reticulum_meshchatx-*-py3-none-any.whl
```

The wheel pulls Python dependencies automatically. Building cryptography can take several minutes on Android.

### Run

```
meshchatx
```

(meshchat is a compatibility alias for the same entry point.)

Open http://localhost:8000 in the Android browser.

## Install from source

Use this path for development or when no wheel fits your setup.

### System packages

```
pkg upgrade
pkg install git
pkg install nodejs-lts
pkg install python
pkg install rust
pkg install binutils
pkg install build-essential
```

### pnpm

```
corepack enable
corepack prepare pnpm@latest --activate
```

### Clone and build

```
git clone https://github.com/Quad4-Software/MeshChatX.git
cd MeshChatX
pip install uv
uv sync --group dev
pnpm install
pnpm run build-frontend
uv build --wheel
pip install dist/*.whl
```

### Run

```
meshchatx
```

(meshchat is a compatibility alias for the same entry point.)

## Configuration notes

> Note: The default AutoInterface may not work on your Android device. Configure another interface such as TCPClientInterface in the settings.

---

## Meta Quest (SideQuest)

The MeshChatX Android APK runs on Meta Quest 2, Quest 3, Quest 3S, and Quest Pro. Quest headsets run a modified Android runtime, so the same universal APK published for phones and tablets can be installed by sideloading.

MeshChatX opens as a **2D panel** inside your VR environment. It is not a native VR application. You get the full MeshChatX web UI in a floating window while you remain in your Quest home space.


## What you need

- A Meta Quest 2 or newer headset
- A Meta account with **Developer Mode** enabled
- [SideQuest](https://sidequestvr.com/) on your PC (desktop app) or access to the SideQuest web installer
- A USB-C cable (for wired sideloading) or a working wireless ADB setup

## Get the APK

Download the latest signed Android APK from the [MeshChatX releases page](https://github.com/Quad4-Software/MeshChatX/releases). Release assets are named like ReticulumMeshChatX-v*-android-universal.apk.

You can also build the APK yourself. See [android/README.md](https://github.com/Quad4-Software/MeshChatX/blob/master/android/README.md).

## Enable Developer Mode

1. Install the Meta Horizon app on your phone and pair your headset.
2. Open **Menu** -> **Devices** -> select your headset -> **Developer Mode**.
3. Turn Developer Mode on and accept the prompt on the headset if asked.

Developer Mode is required for sideloading and for SideQuest to see the device.

## Install with SideQuest

Wired install (typical path):

1. Connect the Quest to your PC with USB-C.
2. Put on the headset. Accept **Allow USB debugging** when Meta prompts you.
3. Open the SideQuest desktop app. Confirm the headset shows as connected (green dot).
4. Click **Install APK file from folder on computer** (or drag the APK onto SideQuest).
5. Select the ReticulumMeshChatX-v*-android-universal.apk file you downloaded.

Wireless ADB works when your PC and headset share a network and SideQuest can pair over Wi-Fi. Follow SideQuest's wireless pairing steps if you prefer that over USB.

## Launch on the headset

1. Open the **Apps** library on the Quest.
2. Filter to **Unknown Sources** (or **Unknown** on newer Horizon builds).
3. Select **MeshChatX**.

The app opens as a 2D panel. Grant microphone permission if you plan to use LXST calls.

## First run

MeshChatX stores data under the Android app sandbox like any other APK build. Add a Reticulum interface from **Interfaces** before you expect mesh traffic. Quest Wi-Fi only reaches your LAN and the internet. It does not replace a mesh uplink unless you configure one (for example a TCP client to a known peer).

For native Android builds (not Termux), see [android/README.md](https://github.com/Quad4-Software/MeshChatX/blob/master/android/README.md).

---

## Linux sandboxing

This page shows how to run **meshchatx** under **Firejail** or **Bubblewrap** (bwrap) on Linux. The legacy CLI name **meshchat** installs the same entry point and can be substituted in these examples. Use this when you install MeshChatX natively (wheel, package, or Poetry) and want an extra layer of filesystem and process isolation compared to running the binary directly.

These tools do **not** replace a full virtual machine or hardware-enforced boundary. They reduce exposure of your home directory and other paths the process can write to, when you configure them with tight whitelists or bind mounts.

MeshChatX also applies optional **in-process** Linux sandboxes when available:

- **Landlock** for filesystem path rules (MESHCHAT_LANDLOCK=0 to disable)
- **Seccomp-BPF** syscall denylist via libseccomp (MESHCHAT_SECCOMP=0 to disable)

Those layers fall back cleanly when unsupported. Firejail and Bubblewrap remain useful as an outer wrapper.

**Landlock and user-local tools:** When Landlock is active, MeshChatX whitelists common pipx paths (~/.local/bin, ~/.local/share/pipx) and Argos Translate data under ~/.local/share/argos-translate so local translation and similar CLIs keep working. Tools installed elsewhere (for example only under ~/.nvm) or symlink shims that point outside those trees may still fail with permission errors. Disable Landlock temporarily with MESHCHAT_LANDLOCK=0 while debugging PATH-only failures.

**Landlock and USB serial:** Landlock read roots include /sys so pyserial can read USB product strings for RNode listing. /dev is already a write root (including IOCTL_DEV on ABI 5+) so opening /dev/ttyACM* still works. Seccomp does not block serial ioctl. The user still needs dialout (or equivalent) group membership, and Ubuntu/Kubuntu brltty can steal CDC ACM devices before MeshChatX sees them.

**Landlock and custom code:** Interface modules under the Reticulum interfaces/ directory, Mesh Server executable pages under identity storage, and installed MeshChatX plugins under storage/plugins stay readable and writable. location_cmd, PipeInterface commands, and Sideband plugin folders must already exist on an allowed root at process start (/usr, ~/.local/bin, storage, or the configured Sideband path). Restart after pointing Sideband at a new directory.

**Containers:** If you already run MeshChatX with Docker or Podman, that is a different isolation model, this document is aimed at **host-installed** meshchatx (or meshchat).

## Prerequisites

Install one or both from your distribution:

- **Firejail:** package name is usually firejail.
- **Bubblewrap:** package name is usually bubblewrap, the binary is bwrap.

You need a working **meshchatx** on your PATH (for example after pipx install, pip install --user, or a distro package). The **meshchat** command is the same binary if both entry points are installed.

Pick a **dedicated data directory** for sandboxed runs so you do not mix permissions or policies with a non-sandboxed install. The examples below use:

```bash
DATA="${XDG_DATA_HOME:-$HOME/.local/share}/meshchatx-sandbox"
mkdir -p "$DATA/storage" "$DATA/.reticulum"
```

Adjust paths if you prefer another location.

## Firejail

Firejail applies a profile (or defaults) on top of your command. For MeshChatX you typically want:

- **Network** left available so Reticulum and the web UI can work (do not use --net=none unless you know you need it).
- **Writable** only your chosen data directory (and any other paths the app needs).

### Installed meshchatx (pip, pipx, or system package)

```bash
DATA="${XDG_DATA_HOME:-$HOME/.local/share}/meshchatx-sandbox"
mkdir -p "$DATA/storage" "$DATA/.reticulum"

firejail --quiet \
  --whitelist="$DATA" \
  meshchatx --headless --host 127.0.0.1 \
    --storage-dir="$DATA/storage" \
    --reticulum-config-dir="$DATA/.reticulum"
```

If the default profile blocks something MeshChatX needs, you can start from a looser base and tighten later, for example:

```bash
firejail --noprofile --whitelist="$DATA" \
  meshchatx --headless --host 127.0.0.1 \
    --storage-dir="$DATA/storage" \
    --reticulum-config-dir="$DATA/.reticulum"
```

--noprofile disables many Firejail restrictions. Treat it as a stepping stone, not the final hardening.

### From source with UV

Poetry needs the project tree and the virtualenv. Example:

```bash
cd /path/to/reticulum-meshchatX
DATA="${XDG_DATA_HOME:-$HOME/.local/share}/meshchatx-sandbox"
VENV="$(pwd)/.venv"
mkdir -p "$DATA/storage" "$DATA/.reticulum"

firejail --quiet \
  --whitelist="$(pwd)" \
  --whitelist="$VENV" \
  --whitelist="$DATA" \
  uv run python -m meshchatx.meshchat --headless --host 127.0.0.1 \
    --storage-dir="$DATA/storage" \
    --reticulum-config-dir="$DATA/.reticulum"
```

You may need extra --whitelist= entries if UV or dependencies read config elsewhere (for example under $HOME/.config).

### USB serial (RNode or similar)

If Reticulum talks to a radio over a serial device, Firejail may need explicit access to TTY devices, for example:

```bash
firejail --noblacklist=/dev/ttyACM0 --noblacklist=/dev/ttyUSB0 \
  ...
```

Use the device nodes your system actually exposes (dmesg, ls /dev/tty*).

## Bubblewrap (bwrap)

Bubblewrap does not ship profiles, so you must list every mount and option. The pattern below keeps the **whole root filesystem read-only**, mounts a **writable tmpfs** on /tmp, and makes **only** your data directory writable at its normal path. **Network namespaces are not changed**, so Reticulum and TCP/UDP behave like an unsandboxed process unless you add --unshare-net (which usually breaks mesh networking).

### Installed meshchatx

```bash
DATA="${XDG_DATA_HOME:-$HOME/.local/share}/meshchatx-sandbox"
mkdir -p "$DATA/storage" "$DATA/.reticulum"

exec bwrap \
  --die-with-parent \
  --new-session \
  --proc /proc \
  --dev /dev \
  --ro-bind / / \
  --tmpfs /tmp \
  --bind "$DATA" "$DATA" \
  --uid "$(id -u)" --gid "$(id -g)" \
  meshchatx --headless --host 127.0.0.1 \
    --storage-dir="$DATA/storage" \
    --reticulum-config-dir="$DATA/.reticulum"
```

Notes:

- If meshchatx lives only inside a venv that is **not** under $DATA, the read-only root still allows **reading** that path, you do not have to bind-mount the venv separately unless you also need writes there.
- Distributions that merge / and /usr (merged-usr) still work with --ro-bind / / on typical glibc setups. If bwrap fails with missing library paths, add the extra --ro-bind lines your distro documents (for example /lib64).

### From source with UV

Bind the repository and the UV venv read-only, and keep DATA writable:

```bash
cd /path/to/reticulum-meshchatX
DATA="${XDG_DATA_HOME:-$HOME/.local/share}/meshchatx-sandbox"
VENV="$(pwd)/.venv"
mkdir -p "$DATA/storage" "$DATA/.reticulum"
PROJ="$(pwd)"

exec bwrap \
  --die-with-parent \
  --new-session \
  --proc /proc \
  --dev /dev \
  --ro-bind / / \
  --tmpfs /tmp \
  --bind "$DATA" "$DATA" \
  --ro-bind "$PROJ" "$PROJ" \
  --ro-bind "$VENV" "$VENV" \
  --uid "$(id -u)" --gid "$(id -g)" \
  --setenv PATH "$VENV/bin:$PATH" \
  --chdir "$PROJ" \
  uv run python -m meshchatx.meshchat --headless --host 127.0.0.1 \
    --storage-dir="$DATA/storage" \
    --reticulum-config-dir="$DATA/.reticulum"
```

uv itself must be reachable on PATH inside the sandbox (often under /usr or $HOME/.local/bin, both visible with --ro-bind / /). If uv run fails because it cannot read ~/.cache/uv, add a read-only bind for that directory or invoke the venv interpreter directly instead of uv run:

```bash
exec bwrap \
  ... same mounts as above ... \
  --setenv PATH "$VENV/bin:$PATH" \
  --chdir "$PROJ" \
  meshchatx --headless --host 127.0.0.1 \
    --storage-dir="$DATA/storage" \
    --reticulum-config-dir="$DATA/.reticulum"
```

(Use the meshchatx script, the legacy meshchat alias, or python -m entry point from $VENV/bin if your install exposes it there.)

### USB serial under Bubblewrap

You may need a clearer view of devices than the minimal --dev /dev provides. Options include --dev-bind /dev /dev (broader device exposure) or binding only the specific character device. Balance convenience against attack surface.